ha-mcp-dev 7.4.1.dev438__tar.gz → 7.4.1.dev439__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {ha_mcp_dev-7.4.1.dev438/src/ha_mcp_dev.egg-info → ha_mcp_dev-7.4.1.dev439}/PKG-INFO +1 -1
- {ha_mcp_dev-7.4.1.dev438 → ha_mcp_dev-7.4.1.dev439}/pyproject.toml +1 -1
- {ha_mcp_dev-7.4.1.dev438 → ha_mcp_dev-7.4.1.dev439}/src/ha_mcp/settings_ui.py +51 -27
- ha_mcp_dev-7.4.1.dev439/src/ha_mcp/utils/data_paths.py +135 -0
- {ha_mcp_dev-7.4.1.dev438 → ha_mcp_dev-7.4.1.dev439}/src/ha_mcp/utils/usage_logger.py +30 -13
- {ha_mcp_dev-7.4.1.dev438 → ha_mcp_dev-7.4.1.dev439/src/ha_mcp_dev.egg-info}/PKG-INFO +1 -1
- {ha_mcp_dev-7.4.1.dev438 → ha_mcp_dev-7.4.1.dev439}/src/ha_mcp_dev.egg-info/SOURCES.txt +1 -0
- {ha_mcp_dev-7.4.1.dev438 → ha_mcp_dev-7.4.1.dev439}/LICENSE +0 -0
- {ha_mcp_dev-7.4.1.dev438 → ha_mcp_dev-7.4.1.dev439}/MANIFEST.in +0 -0
- {ha_mcp_dev-7.4.1.dev438 → ha_mcp_dev-7.4.1.dev439}/README.md +0 -0
- {ha_mcp_dev-7.4.1.dev438 → ha_mcp_dev-7.4.1.dev439}/setup.cfg +0 -0
- {ha_mcp_dev-7.4.1.dev438 → ha_mcp_dev-7.4.1.dev439}/src/ha_mcp/__init__.py +0 -0
- {ha_mcp_dev-7.4.1.dev438 → ha_mcp_dev-7.4.1.dev439}/src/ha_mcp/__main__.py +0 -0
- {ha_mcp_dev-7.4.1.dev438 → ha_mcp_dev-7.4.1.dev439}/src/ha_mcp/_pypi_marker +0 -0
- {ha_mcp_dev-7.4.1.dev438 → ha_mcp_dev-7.4.1.dev439}/src/ha_mcp/_version.py +0 -0
- {ha_mcp_dev-7.4.1.dev438 → ha_mcp_dev-7.4.1.dev439}/src/ha_mcp/auth/__init__.py +0 -0
- {ha_mcp_dev-7.4.1.dev438 → ha_mcp_dev-7.4.1.dev439}/src/ha_mcp/auth/consent_form.py +0 -0
- {ha_mcp_dev-7.4.1.dev438 → ha_mcp_dev-7.4.1.dev439}/src/ha_mcp/auth/provider.py +0 -0
- {ha_mcp_dev-7.4.1.dev438 → ha_mcp_dev-7.4.1.dev439}/src/ha_mcp/client/__init__.py +0 -0
- {ha_mcp_dev-7.4.1.dev438 → ha_mcp_dev-7.4.1.dev439}/src/ha_mcp/client/rest_client.py +0 -0
- {ha_mcp_dev-7.4.1.dev438 → ha_mcp_dev-7.4.1.dev439}/src/ha_mcp/client/websocket_client.py +0 -0
- {ha_mcp_dev-7.4.1.dev438 → ha_mcp_dev-7.4.1.dev439}/src/ha_mcp/client/websocket_listener.py +0 -0
- {ha_mcp_dev-7.4.1.dev438 → ha_mcp_dev-7.4.1.dev439}/src/ha_mcp/config.py +0 -0
- {ha_mcp_dev-7.4.1.dev438 → ha_mcp_dev-7.4.1.dev439}/src/ha_mcp/errors.py +0 -0
- {ha_mcp_dev-7.4.1.dev438 → ha_mcp_dev-7.4.1.dev439}/src/ha_mcp/py.typed +0 -0
- {ha_mcp_dev-7.4.1.dev438 → ha_mcp_dev-7.4.1.dev439}/src/ha_mcp/resources/skills-vendor/.claude/settings.json +0 -0
- {ha_mcp_dev-7.4.1.dev438 → ha_mcp_dev-7.4.1.dev439}/src/ha_mcp/resources/skills-vendor/.claude-plugin/marketplace.json +0 -0
- {ha_mcp_dev-7.4.1.dev438 → ha_mcp_dev-7.4.1.dev439}/src/ha_mcp/resources/skills-vendor/.claude-plugin/plugin.json +0 -0
- {ha_mcp_dev-7.4.1.dev438 → ha_mcp_dev-7.4.1.dev439}/src/ha_mcp/resources/skills-vendor/.github/ISSUE_TEMPLATE/skill-rca.md +0 -0
- {ha_mcp_dev-7.4.1.dev438 → ha_mcp_dev-7.4.1.dev439}/src/ha_mcp/resources/skills-vendor/AGENTS.md +0 -0
- {ha_mcp_dev-7.4.1.dev438 → ha_mcp_dev-7.4.1.dev439}/src/ha_mcp/resources/skills-vendor/CLAUDE.md +0 -0
- {ha_mcp_dev-7.4.1.dev438 → ha_mcp_dev-7.4.1.dev439}/src/ha_mcp/resources/skills-vendor/CONTRIBUTING.md +0 -0
- {ha_mcp_dev-7.4.1.dev438 → ha_mcp_dev-7.4.1.dev439}/src/ha_mcp/resources/skills-vendor/LICENSE +0 -0
- {ha_mcp_dev-7.4.1.dev438 → ha_mcp_dev-7.4.1.dev439}/src/ha_mcp/resources/skills-vendor/README.md +0 -0
- {ha_mcp_dev-7.4.1.dev438 → ha_mcp_dev-7.4.1.dev439}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/SKILL.md +0 -0
- {ha_mcp_dev-7.4.1.dev438 → ha_mcp_dev-7.4.1.dev439}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/evals/evals.json +0 -0
- {ha_mcp_dev-7.4.1.dev438 → ha_mcp_dev-7.4.1.dev439}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/automation-patterns.md +0 -0
- {ha_mcp_dev-7.4.1.dev438 → ha_mcp_dev-7.4.1.dev439}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/dashboard-cards.md +0 -0
- {ha_mcp_dev-7.4.1.dev438 → ha_mcp_dev-7.4.1.dev439}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/dashboard-guide.md +0 -0
- {ha_mcp_dev-7.4.1.dev438 → ha_mcp_dev-7.4.1.dev439}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/device-control.md +0 -0
- {ha_mcp_dev-7.4.1.dev438 → ha_mcp_dev-7.4.1.dev439}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/domain-docs.md +0 -0
- {ha_mcp_dev-7.4.1.dev438 → ha_mcp_dev-7.4.1.dev439}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/examples.yaml +0 -0
- {ha_mcp_dev-7.4.1.dev438 → ha_mcp_dev-7.4.1.dev439}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/helper-selection.md +0 -0
- {ha_mcp_dev-7.4.1.dev438 → ha_mcp_dev-7.4.1.dev439}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/safe-refactoring.md +0 -0
- {ha_mcp_dev-7.4.1.dev438 → ha_mcp_dev-7.4.1.dev439}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/template-guidelines.md +0 -0
- {ha_mcp_dev-7.4.1.dev438 → ha_mcp_dev-7.4.1.dev439}/src/ha_mcp/server.py +0 -0
- {ha_mcp_dev-7.4.1.dev438 → ha_mcp_dev-7.4.1.dev439}/src/ha_mcp/smoke_test.py +0 -0
- {ha_mcp_dev-7.4.1.dev438 → ha_mcp_dev-7.4.1.dev439}/src/ha_mcp/tools/__init__.py +0 -0
- {ha_mcp_dev-7.4.1.dev438 → ha_mcp_dev-7.4.1.dev439}/src/ha_mcp/tools/backup.py +0 -0
- {ha_mcp_dev-7.4.1.dev438 → ha_mcp_dev-7.4.1.dev439}/src/ha_mcp/tools/best_practice_checker.py +0 -0
- {ha_mcp_dev-7.4.1.dev438 → ha_mcp_dev-7.4.1.dev439}/src/ha_mcp/tools/device_control.py +0 -0
- {ha_mcp_dev-7.4.1.dev438 → ha_mcp_dev-7.4.1.dev439}/src/ha_mcp/tools/enhanced.py +0 -0
- {ha_mcp_dev-7.4.1.dev438 → ha_mcp_dev-7.4.1.dev439}/src/ha_mcp/tools/helpers.py +0 -0
- {ha_mcp_dev-7.4.1.dev438 → ha_mcp_dev-7.4.1.dev439}/src/ha_mcp/tools/reference_validator.py +0 -0
- {ha_mcp_dev-7.4.1.dev438 → ha_mcp_dev-7.4.1.dev439}/src/ha_mcp/tools/registry.py +0 -0
- {ha_mcp_dev-7.4.1.dev438 → ha_mcp_dev-7.4.1.dev439}/src/ha_mcp/tools/smart_search.py +0 -0
- {ha_mcp_dev-7.4.1.dev438 → ha_mcp_dev-7.4.1.dev439}/src/ha_mcp/tools/tools_addons.py +0 -0
- {ha_mcp_dev-7.4.1.dev438 → ha_mcp_dev-7.4.1.dev439}/src/ha_mcp/tools/tools_areas.py +0 -0
- {ha_mcp_dev-7.4.1.dev438 → ha_mcp_dev-7.4.1.dev439}/src/ha_mcp/tools/tools_blueprints.py +0 -0
- {ha_mcp_dev-7.4.1.dev438 → ha_mcp_dev-7.4.1.dev439}/src/ha_mcp/tools/tools_bug_report.py +0 -0
- {ha_mcp_dev-7.4.1.dev438 → ha_mcp_dev-7.4.1.dev439}/src/ha_mcp/tools/tools_calendar.py +0 -0
- {ha_mcp_dev-7.4.1.dev438 → ha_mcp_dev-7.4.1.dev439}/src/ha_mcp/tools/tools_camera.py +0 -0
- {ha_mcp_dev-7.4.1.dev438 → ha_mcp_dev-7.4.1.dev439}/src/ha_mcp/tools/tools_categories.py +0 -0
- {ha_mcp_dev-7.4.1.dev438 → ha_mcp_dev-7.4.1.dev439}/src/ha_mcp/tools/tools_config_automations.py +0 -0
- {ha_mcp_dev-7.4.1.dev438 → ha_mcp_dev-7.4.1.dev439}/src/ha_mcp/tools/tools_config_dashboards.py +0 -0
- {ha_mcp_dev-7.4.1.dev438 → ha_mcp_dev-7.4.1.dev439}/src/ha_mcp/tools/tools_config_entry_flow.py +0 -0
- {ha_mcp_dev-7.4.1.dev438 → ha_mcp_dev-7.4.1.dev439}/src/ha_mcp/tools/tools_config_helpers.py +0 -0
- {ha_mcp_dev-7.4.1.dev438 → ha_mcp_dev-7.4.1.dev439}/src/ha_mcp/tools/tools_config_scripts.py +0 -0
- {ha_mcp_dev-7.4.1.dev438 → ha_mcp_dev-7.4.1.dev439}/src/ha_mcp/tools/tools_energy.py +0 -0
- {ha_mcp_dev-7.4.1.dev438 → ha_mcp_dev-7.4.1.dev439}/src/ha_mcp/tools/tools_entities.py +0 -0
- {ha_mcp_dev-7.4.1.dev438 → ha_mcp_dev-7.4.1.dev439}/src/ha_mcp/tools/tools_filesystem.py +0 -0
- {ha_mcp_dev-7.4.1.dev438 → ha_mcp_dev-7.4.1.dev439}/src/ha_mcp/tools/tools_groups.py +0 -0
- {ha_mcp_dev-7.4.1.dev438 → ha_mcp_dev-7.4.1.dev439}/src/ha_mcp/tools/tools_hacs.py +0 -0
- {ha_mcp_dev-7.4.1.dev438 → ha_mcp_dev-7.4.1.dev439}/src/ha_mcp/tools/tools_history.py +0 -0
- {ha_mcp_dev-7.4.1.dev438 → ha_mcp_dev-7.4.1.dev439}/src/ha_mcp/tools/tools_integrations.py +0 -0
- {ha_mcp_dev-7.4.1.dev438 → ha_mcp_dev-7.4.1.dev439}/src/ha_mcp/tools/tools_labels.py +0 -0
- {ha_mcp_dev-7.4.1.dev438 → ha_mcp_dev-7.4.1.dev439}/src/ha_mcp/tools/tools_mcp_component.py +0 -0
- {ha_mcp_dev-7.4.1.dev438 → ha_mcp_dev-7.4.1.dev439}/src/ha_mcp/tools/tools_registry.py +0 -0
- {ha_mcp_dev-7.4.1.dev438 → ha_mcp_dev-7.4.1.dev439}/src/ha_mcp/tools/tools_resources.py +0 -0
- {ha_mcp_dev-7.4.1.dev438 → ha_mcp_dev-7.4.1.dev439}/src/ha_mcp/tools/tools_search.py +0 -0
- {ha_mcp_dev-7.4.1.dev438 → ha_mcp_dev-7.4.1.dev439}/src/ha_mcp/tools/tools_service.py +0 -0
- {ha_mcp_dev-7.4.1.dev438 → ha_mcp_dev-7.4.1.dev439}/src/ha_mcp/tools/tools_services.py +0 -0
- {ha_mcp_dev-7.4.1.dev438 → ha_mcp_dev-7.4.1.dev439}/src/ha_mcp/tools/tools_system.py +0 -0
- {ha_mcp_dev-7.4.1.dev438 → ha_mcp_dev-7.4.1.dev439}/src/ha_mcp/tools/tools_todo.py +0 -0
- {ha_mcp_dev-7.4.1.dev438 → ha_mcp_dev-7.4.1.dev439}/src/ha_mcp/tools/tools_traces.py +0 -0
- {ha_mcp_dev-7.4.1.dev438 → ha_mcp_dev-7.4.1.dev439}/src/ha_mcp/tools/tools_updates.py +0 -0
- {ha_mcp_dev-7.4.1.dev438 → ha_mcp_dev-7.4.1.dev439}/src/ha_mcp/tools/tools_utility.py +0 -0
- {ha_mcp_dev-7.4.1.dev438 → ha_mcp_dev-7.4.1.dev439}/src/ha_mcp/tools/tools_voice_assistant.py +0 -0
- {ha_mcp_dev-7.4.1.dev438 → ha_mcp_dev-7.4.1.dev439}/src/ha_mcp/tools/tools_yaml_config.py +0 -0
- {ha_mcp_dev-7.4.1.dev438 → ha_mcp_dev-7.4.1.dev439}/src/ha_mcp/tools/tools_zones.py +0 -0
- {ha_mcp_dev-7.4.1.dev438 → ha_mcp_dev-7.4.1.dev439}/src/ha_mcp/tools/util_helpers.py +0 -0
- {ha_mcp_dev-7.4.1.dev438 → ha_mcp_dev-7.4.1.dev439}/src/ha_mcp/transforms/__init__.py +0 -0
- {ha_mcp_dev-7.4.1.dev438 → ha_mcp_dev-7.4.1.dev439}/src/ha_mcp/transforms/categorized_search.py +0 -0
- {ha_mcp_dev-7.4.1.dev438 → ha_mcp_dev-7.4.1.dev439}/src/ha_mcp/utils/__init__.py +0 -0
- {ha_mcp_dev-7.4.1.dev438 → ha_mcp_dev-7.4.1.dev439}/src/ha_mcp/utils/config_hash.py +0 -0
- {ha_mcp_dev-7.4.1.dev438 → ha_mcp_dev-7.4.1.dev439}/src/ha_mcp/utils/domain_handlers.py +0 -0
- {ha_mcp_dev-7.4.1.dev438 → ha_mcp_dev-7.4.1.dev439}/src/ha_mcp/utils/fuzzy_search.py +0 -0
- {ha_mcp_dev-7.4.1.dev438 → ha_mcp_dev-7.4.1.dev439}/src/ha_mcp/utils/kill_signal_diagnostics.py +0 -0
- {ha_mcp_dev-7.4.1.dev438 → ha_mcp_dev-7.4.1.dev439}/src/ha_mcp/utils/operation_manager.py +0 -0
- {ha_mcp_dev-7.4.1.dev438 → ha_mcp_dev-7.4.1.dev439}/src/ha_mcp/utils/python_sandbox.py +0 -0
- {ha_mcp_dev-7.4.1.dev438 → ha_mcp_dev-7.4.1.dev439}/src/ha_mcp_dev.egg-info/dependency_links.txt +0 -0
- {ha_mcp_dev-7.4.1.dev438 → ha_mcp_dev-7.4.1.dev439}/src/ha_mcp_dev.egg-info/entry_points.txt +0 -0
- {ha_mcp_dev-7.4.1.dev438 → ha_mcp_dev-7.4.1.dev439}/src/ha_mcp_dev.egg-info/requires.txt +0 -0
- {ha_mcp_dev-7.4.1.dev438 → ha_mcp_dev-7.4.1.dev439}/src/ha_mcp_dev.egg-info/top_level.txt +0 -0
- {ha_mcp_dev-7.4.1.dev438 → ha_mcp_dev-7.4.1.dev439}/tests/__init__.py +0 -0
- {ha_mcp_dev-7.4.1.dev438 → ha_mcp_dev-7.4.1.dev439}/tests/test_constants.py +0 -0
- {ha_mcp_dev-7.4.1.dev438 → ha_mcp_dev-7.4.1.dev439}/tests/test_env_manager.py +0 -0
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "ha-mcp-dev"
|
|
7
|
-
version = "7.4.1.
|
|
7
|
+
version = "7.4.1.dev439"
|
|
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"
|
|
@@ -19,8 +19,10 @@ import httpx
|
|
|
19
19
|
from starlette.requests import Request
|
|
20
20
|
from starlette.responses import HTMLResponse, JSONResponse
|
|
21
21
|
|
|
22
|
+
from ._version import is_running_in_addon
|
|
22
23
|
from .errors import ErrorCode, create_error_response
|
|
23
24
|
from .transforms import DEFAULT_PINNED_TOOLS
|
|
25
|
+
from .utils.data_paths import get_data_dir
|
|
24
26
|
|
|
25
27
|
if TYPE_CHECKING:
|
|
26
28
|
from fastmcp import FastMCP
|
|
@@ -99,36 +101,38 @@ FEATURE_GATED_TOOLS: dict[str, dict[str, str]] = {
|
|
|
99
101
|
}
|
|
100
102
|
|
|
101
103
|
|
|
102
|
-
def
|
|
103
|
-
"""Return
|
|
104
|
+
def _get_config_path() -> Path:
|
|
105
|
+
"""Return the path to the tool config JSON file.
|
|
104
106
|
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
setups (and macOS dev environments) have a ``/data`` directory that
|
|
109
|
-
isn't the add-on data dir.
|
|
107
|
+
Delegates directory resolution to :func:`utils.data_paths.get_data_dir`,
|
|
108
|
+
which handles ``HA_MCP_CONFIG_DIR`` override, add-on ``/data``,
|
|
109
|
+
home-dir, and tmpdir fallback (memoized).
|
|
110
110
|
"""
|
|
111
|
-
return
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
def _get_config_path() -> Path:
|
|
115
|
-
"""Return the path to the tool config JSON file."""
|
|
116
|
-
if _is_addon():
|
|
117
|
-
return Path("/data") / "tool_config.json"
|
|
118
|
-
home_dir = Path.home() / ".ha-mcp"
|
|
119
|
-
home_dir.mkdir(parents=True, exist_ok=True)
|
|
120
|
-
return home_dir / "tool_config.json"
|
|
111
|
+
return get_data_dir() / "tool_config.json"
|
|
121
112
|
|
|
122
113
|
|
|
123
114
|
def load_tool_config(settings: Settings | None = None) -> dict[str, Any]:
|
|
124
115
|
"""Load persisted tool config, seeding from env vars if no file exists."""
|
|
125
116
|
path = _get_config_path()
|
|
126
|
-
|
|
117
|
+
# ``Path.exists()`` only swallows ``ENOENT/ENOTDIR/EBADF/ELOOP``; an
|
|
118
|
+
# ``EACCES`` (e.g. ``HA_MCP_CONFIG_DIR`` pointing at a dir that exists
|
|
119
|
+
# but isn't readable by the runtime UID) propagates. Read directly and
|
|
120
|
+
# treat ``FileNotFoundError`` as "no config yet"; log other ``OSError``s.
|
|
121
|
+
try:
|
|
122
|
+
raw = path.read_text()
|
|
123
|
+
except FileNotFoundError:
|
|
124
|
+
raw = None
|
|
125
|
+
except OSError:
|
|
126
|
+
logger.warning("Cannot read tool config at %s", path, exc_info=True)
|
|
127
|
+
raw = None
|
|
128
|
+
|
|
129
|
+
if raw is not None:
|
|
127
130
|
try:
|
|
128
|
-
result: dict[str, Any] = json.loads(
|
|
131
|
+
result: dict[str, Any] = json.loads(raw)
|
|
132
|
+
except json.JSONDecodeError:
|
|
133
|
+
logger.warning("Tool config at %s is not valid JSON; ignoring.", path)
|
|
134
|
+
else:
|
|
129
135
|
return result
|
|
130
|
-
except (OSError, json.JSONDecodeError):
|
|
131
|
-
logger.warning("Failed to read tool config from %s", path)
|
|
132
136
|
|
|
133
137
|
if settings is None:
|
|
134
138
|
return {}
|
|
@@ -156,14 +160,23 @@ def load_tool_config(settings: Settings | None = None) -> dict[str, Any]:
|
|
|
156
160
|
return {}
|
|
157
161
|
|
|
158
162
|
|
|
159
|
-
def save_tool_config(config: dict[str, Any]) ->
|
|
160
|
-
"""Persist tool config to disk.
|
|
163
|
+
def save_tool_config(config: dict[str, Any]) -> bool:
|
|
164
|
+
"""Persist tool config to disk.
|
|
165
|
+
|
|
166
|
+
Returns True on success, False on failure (read-only filesystem,
|
|
167
|
+
permission denied, etc.). Caller is responsible for surfacing the
|
|
168
|
+
failure to the user — the HTTP route at ``_save_tools`` returns 500
|
|
169
|
+
so the UI's ``saveConfig`` shows "Save failed!" instead of the
|
|
170
|
+
misleading "Saved — restart required".
|
|
171
|
+
"""
|
|
161
172
|
path = _get_config_path()
|
|
162
173
|
try:
|
|
163
174
|
path.write_text(json.dumps(config, indent=2))
|
|
164
|
-
logger.info("Saved tool config to %s", path)
|
|
165
175
|
except OSError:
|
|
166
176
|
logger.exception("Failed to save tool config to %s", path)
|
|
177
|
+
return False
|
|
178
|
+
logger.info("Saved tool config to %s", path)
|
|
179
|
+
return True
|
|
167
180
|
|
|
168
181
|
|
|
169
182
|
async def _get_tool_metadata(server: HomeAssistantSmartMCPServer) -> list[dict[str, Any]]:
|
|
@@ -760,7 +773,18 @@ def register_settings_routes(
|
|
|
760
773
|
|
|
761
774
|
config = load_tool_config()
|
|
762
775
|
config["tools"] = states
|
|
763
|
-
save_tool_config(config)
|
|
776
|
+
if not save_tool_config(config):
|
|
777
|
+
return JSONResponse(
|
|
778
|
+
create_error_response(
|
|
779
|
+
ErrorCode.INTERNAL_ERROR,
|
|
780
|
+
"Failed to persist tool config to disk",
|
|
781
|
+
suggestions=[
|
|
782
|
+
"Set HA_MCP_CONFIG_DIR to a writable path (read-only filesystem?)",
|
|
783
|
+
"Check the server logs for the underlying OSError",
|
|
784
|
+
],
|
|
785
|
+
),
|
|
786
|
+
status_code=500,
|
|
787
|
+
)
|
|
764
788
|
|
|
765
789
|
disabled_count = sum(1 for s in states.values() if s == "disabled")
|
|
766
790
|
pinned_count = sum(1 for s in states.values() if s == "pinned")
|
|
@@ -823,11 +847,11 @@ def register_settings_routes(
|
|
|
823
847
|
|
|
824
848
|
async def _settings_info(_: Request) -> JSONResponse:
|
|
825
849
|
return JSONResponse({
|
|
826
|
-
"is_addon":
|
|
850
|
+
"is_addon": is_running_in_addon(),
|
|
827
851
|
})
|
|
828
852
|
|
|
829
853
|
secret_prefix = secret_path.rstrip("/") if secret_path else ""
|
|
830
|
-
is_addon =
|
|
854
|
+
is_addon = is_running_in_addon()
|
|
831
855
|
|
|
832
856
|
if not is_addon and not secret_prefix:
|
|
833
857
|
logger.warning(
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
"""Resolve a writable directory for ha-mcp persistent data.
|
|
2
|
+
|
|
3
|
+
Single source of truth for "where does ha-mcp write its files?" — used
|
|
4
|
+
by both ``settings_ui`` (tool config) and ``usage_logger`` (rolling
|
|
5
|
+
JSONL).
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import functools
|
|
11
|
+
import logging
|
|
12
|
+
import os
|
|
13
|
+
import tempfile
|
|
14
|
+
from pathlib import Path
|
|
15
|
+
|
|
16
|
+
from .._version import is_running_in_addon
|
|
17
|
+
|
|
18
|
+
logger = logging.getLogger(__name__)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
@functools.lru_cache(maxsize=1)
|
|
22
|
+
def get_data_dir() -> Path:
|
|
23
|
+
"""Return a writable directory for ha-mcp persistent data (memoized).
|
|
24
|
+
|
|
25
|
+
Resolution order:
|
|
26
|
+
|
|
27
|
+
1. ``HA_MCP_CONFIG_DIR`` env var — explicit override, e.g. for hardened
|
|
28
|
+
Docker setups bind-mounting a writable volume into a
|
|
29
|
+
``read_only: true`` container.
|
|
30
|
+
2. ``/data`` — Home Assistant add-on (``SUPERVISOR_TOKEN`` set; writable
|
|
31
|
+
supervisor data dir).
|
|
32
|
+
3. ``~/.ha-mcp`` — standard install. Skipped when ``HA_MCP_CONFIG_DIR``
|
|
33
|
+
was set but failed: an explicit override means "use this exact
|
|
34
|
+
location", and silently writing to ``$HOME`` instead would surprise
|
|
35
|
+
users who chose the override deliberately.
|
|
36
|
+
4. ``<tempdir>/ha-mcp`` — last-resort fallback when the previously
|
|
37
|
+
chosen step fails (read-only filesystem; ``HOME`` unset so
|
|
38
|
+
``Path.home()`` resolves to ``/``; or ``HA_MCP_CONFIG_DIR`` set but
|
|
39
|
+
its mkdir raises). Loses persistence across restarts but lets the
|
|
40
|
+
server start; users wanting persistence should set
|
|
41
|
+
``HA_MCP_CONFIG_DIR`` to a writable path.
|
|
42
|
+
|
|
43
|
+
Memoized so the fallback warning typically emits once at startup
|
|
44
|
+
rather than on every save/load HTTP request. ``lru_cache`` serializes
|
|
45
|
+
its internal dict but does not serialize the wrapped call when the
|
|
46
|
+
cache is empty, so two threads racing on first access (e.g.
|
|
47
|
+
``UsageLogger.__init__`` from a worker thread plus a settings UI HTTP
|
|
48
|
+
handler) may each run ``_resolve_data_dir`` once and emit the warning
|
|
49
|
+
twice. The mkdir calls are idempotent, so this is cosmetic.
|
|
50
|
+
Tests reset via ``get_data_dir.cache_clear()``.
|
|
51
|
+
"""
|
|
52
|
+
return _resolve_data_dir()
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def _resolve_data_dir() -> Path:
|
|
56
|
+
"""Resolve the data directory (uncached); see ``get_data_dir`` for priority."""
|
|
57
|
+
# ``.strip()``: ``HA_MCP_CONFIG_DIR=" "`` is truthy and ``Path(" ")``
|
|
58
|
+
# resolves cwd-relative, which would mkdir a literal whitespace-named
|
|
59
|
+
# directory next to whatever cwd happens to be at startup.
|
|
60
|
+
config_dir_env = os.environ.get("HA_MCP_CONFIG_DIR", "").strip()
|
|
61
|
+
preferred: Path | None = None
|
|
62
|
+
if config_dir_env:
|
|
63
|
+
custom_dir = Path(config_dir_env)
|
|
64
|
+
try:
|
|
65
|
+
custom_dir.mkdir(parents=True, exist_ok=True)
|
|
66
|
+
except OSError as e:
|
|
67
|
+
logger.warning(
|
|
68
|
+
"HA_MCP_CONFIG_DIR=%s could not be prepared (%s: %s); "
|
|
69
|
+
"falling back to a tmpdir.",
|
|
70
|
+
custom_dir,
|
|
71
|
+
type(e).__name__,
|
|
72
|
+
e,
|
|
73
|
+
)
|
|
74
|
+
preferred = custom_dir
|
|
75
|
+
else:
|
|
76
|
+
return custom_dir
|
|
77
|
+
|
|
78
|
+
if is_running_in_addon():
|
|
79
|
+
addon_dir = Path("/data")
|
|
80
|
+
try:
|
|
81
|
+
addon_dir.mkdir(parents=True, exist_ok=True)
|
|
82
|
+
except OSError as e:
|
|
83
|
+
logger.warning(
|
|
84
|
+
"/data is not writable in add-on mode (%s: %s); "
|
|
85
|
+
"falling back. Set HA_MCP_CONFIG_DIR to override.",
|
|
86
|
+
type(e).__name__,
|
|
87
|
+
e,
|
|
88
|
+
)
|
|
89
|
+
if preferred is None:
|
|
90
|
+
preferred = addon_dir
|
|
91
|
+
else:
|
|
92
|
+
# Honor an explicit HA_MCP_CONFIG_DIR override even in add-on
|
|
93
|
+
# mode: if the user set it and its mkdir failed (preferred is
|
|
94
|
+
# not None), fall through to the tmpdir fallback rather than
|
|
95
|
+
# silently writing to /data — they chose the override
|
|
96
|
+
# deliberately.
|
|
97
|
+
if preferred is None:
|
|
98
|
+
return addon_dir
|
|
99
|
+
|
|
100
|
+
if preferred is None:
|
|
101
|
+
home_dir = Path.home() / ".ha-mcp"
|
|
102
|
+
try:
|
|
103
|
+
home_dir.mkdir(parents=True, exist_ok=True)
|
|
104
|
+
except OSError:
|
|
105
|
+
preferred = home_dir
|
|
106
|
+
else:
|
|
107
|
+
return home_dir
|
|
108
|
+
|
|
109
|
+
fallback = Path(tempfile.gettempdir()) / "ha-mcp"
|
|
110
|
+
try:
|
|
111
|
+
fallback.mkdir(parents=True, exist_ok=True)
|
|
112
|
+
except OSError as e:
|
|
113
|
+
# Even the tmpdir is unwritable. Return the path anyway: callers
|
|
114
|
+
# that wrap writes in try/except OSError can degrade gracefully
|
|
115
|
+
# (no persistence, but the server still starts). ``error`` rather
|
|
116
|
+
# than ``warning`` because persistence is silently disabled — the
|
|
117
|
+
# supervisor log viewer surfaces errors more prominently.
|
|
118
|
+
logger.error(
|
|
119
|
+
"Cannot write ha-mcp data to %s or fallback %s (%s: %s); "
|
|
120
|
+
"persistence is disabled. "
|
|
121
|
+
"Set HA_MCP_CONFIG_DIR to a writable path for persistence.",
|
|
122
|
+
preferred,
|
|
123
|
+
fallback,
|
|
124
|
+
type(e).__name__,
|
|
125
|
+
e,
|
|
126
|
+
)
|
|
127
|
+
else:
|
|
128
|
+
logger.warning(
|
|
129
|
+
"Cannot write ha-mcp data to %s (read-only filesystem or HOME unset). "
|
|
130
|
+
"Falling back to %s — data will NOT persist across restarts. "
|
|
131
|
+
"Set HA_MCP_CONFIG_DIR to a writable path for persistence.",
|
|
132
|
+
preferred,
|
|
133
|
+
fallback,
|
|
134
|
+
)
|
|
135
|
+
return fallback
|
|
@@ -13,6 +13,10 @@ from pathlib import Path
|
|
|
13
13
|
from queue import Queue
|
|
14
14
|
from typing import Any
|
|
15
15
|
|
|
16
|
+
from .data_paths import get_data_dir
|
|
17
|
+
|
|
18
|
+
logger = logging.getLogger(__name__)
|
|
19
|
+
|
|
16
20
|
# Default ring buffer size - keeps last N entries in memory
|
|
17
21
|
DEFAULT_RING_BUFFER_SIZE = 200
|
|
18
22
|
|
|
@@ -46,13 +50,15 @@ class StartupLogCollector(logging.Handler):
|
|
|
46
50
|
return
|
|
47
51
|
|
|
48
52
|
with self._lock:
|
|
49
|
-
self._logs.append(
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
53
|
+
self._logs.append(
|
|
54
|
+
{
|
|
55
|
+
"timestamp": datetime.now(UTC).isoformat(),
|
|
56
|
+
"level": record.levelname,
|
|
57
|
+
"logger": record.name,
|
|
58
|
+
"message": record.getMessage(),
|
|
59
|
+
"elapsed_seconds": round(elapsed, 2),
|
|
60
|
+
}
|
|
61
|
+
)
|
|
56
62
|
|
|
57
63
|
def get_logs(self) -> list[dict[str, Any]]:
|
|
58
64
|
"""Get collected startup logs."""
|
|
@@ -115,15 +121,26 @@ class UsageLogger:
|
|
|
115
121
|
if log_file_path:
|
|
116
122
|
self.log_file_path = Path(log_file_path)
|
|
117
123
|
else:
|
|
118
|
-
#
|
|
119
|
-
#
|
|
120
|
-
|
|
124
|
+
# Defer to the shared resolver so logs follow the same precedence
|
|
125
|
+
# as the settings-UI tool config (HA_MCP_CONFIG_DIR > /data >
|
|
126
|
+
# ~/.ha-mcp > tempdir). Avoids polluting the filesystem root when
|
|
127
|
+
# HOME is unset and avoids surprising users who bind-mount a
|
|
128
|
+
# writable volume via HA_MCP_CONFIG_DIR but find logs missing.
|
|
129
|
+
self.log_file_path = get_data_dir() / "logs" / "mcp_usage.jsonl"
|
|
121
130
|
|
|
122
131
|
try:
|
|
123
132
|
self.log_file_path.parent.mkdir(parents=True, exist_ok=True)
|
|
124
|
-
except OSError:
|
|
125
|
-
# Directory creation failed (e.g., read-only filesystem)
|
|
126
|
-
#
|
|
133
|
+
except OSError as e:
|
|
134
|
+
# Directory creation failed (e.g., read-only filesystem). Surface
|
|
135
|
+
# the reason instead of silently dropping every log — operators
|
|
136
|
+
# otherwise see an empty mcp_usage.jsonl with no clue why.
|
|
137
|
+
logger.warning(
|
|
138
|
+
"Usage logging disabled — could not create %s (%s: %s). "
|
|
139
|
+
"Set HA_MCP_CONFIG_DIR to a writable path to enable persistence.",
|
|
140
|
+
self.log_file_path.parent,
|
|
141
|
+
type(e).__name__,
|
|
142
|
+
e,
|
|
143
|
+
)
|
|
127
144
|
self._enabled = False
|
|
128
145
|
|
|
129
146
|
# In-memory ring buffer for fast access to recent logs
|
|
@@ -87,6 +87,7 @@ src/ha_mcp/transforms/__init__.py
|
|
|
87
87
|
src/ha_mcp/transforms/categorized_search.py
|
|
88
88
|
src/ha_mcp/utils/__init__.py
|
|
89
89
|
src/ha_mcp/utils/config_hash.py
|
|
90
|
+
src/ha_mcp/utils/data_paths.py
|
|
90
91
|
src/ha_mcp/utils/domain_handlers.py
|
|
91
92
|
src/ha_mcp/utils/fuzzy_search.py
|
|
92
93
|
src/ha_mcp/utils/kill_signal_diagnostics.py
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{ha_mcp_dev-7.4.1.dev438 → ha_mcp_dev-7.4.1.dev439}/src/ha_mcp/resources/skills-vendor/AGENTS.md
RENAMED
|
File without changes
|
{ha_mcp_dev-7.4.1.dev438 → ha_mcp_dev-7.4.1.dev439}/src/ha_mcp/resources/skills-vendor/CLAUDE.md
RENAMED
|
File without changes
|
|
File without changes
|
{ha_mcp_dev-7.4.1.dev438 → ha_mcp_dev-7.4.1.dev439}/src/ha_mcp/resources/skills-vendor/LICENSE
RENAMED
|
File without changes
|
{ha_mcp_dev-7.4.1.dev438 → ha_mcp_dev-7.4.1.dev439}/src/ha_mcp/resources/skills-vendor/README.md
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{ha_mcp_dev-7.4.1.dev438 → ha_mcp_dev-7.4.1.dev439}/src/ha_mcp/tools/best_practice_checker.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{ha_mcp_dev-7.4.1.dev438 → ha_mcp_dev-7.4.1.dev439}/src/ha_mcp/tools/tools_config_automations.py
RENAMED
|
File without changes
|
{ha_mcp_dev-7.4.1.dev438 → ha_mcp_dev-7.4.1.dev439}/src/ha_mcp/tools/tools_config_dashboards.py
RENAMED
|
File without changes
|
{ha_mcp_dev-7.4.1.dev438 → ha_mcp_dev-7.4.1.dev439}/src/ha_mcp/tools/tools_config_entry_flow.py
RENAMED
|
File without changes
|
{ha_mcp_dev-7.4.1.dev438 → ha_mcp_dev-7.4.1.dev439}/src/ha_mcp/tools/tools_config_helpers.py
RENAMED
|
File without changes
|
{ha_mcp_dev-7.4.1.dev438 → ha_mcp_dev-7.4.1.dev439}/src/ha_mcp/tools/tools_config_scripts.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{ha_mcp_dev-7.4.1.dev438 → ha_mcp_dev-7.4.1.dev439}/src/ha_mcp/tools/tools_voice_assistant.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{ha_mcp_dev-7.4.1.dev438 → ha_mcp_dev-7.4.1.dev439}/src/ha_mcp/transforms/categorized_search.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{ha_mcp_dev-7.4.1.dev438 → ha_mcp_dev-7.4.1.dev439}/src/ha_mcp/utils/kill_signal_diagnostics.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{ha_mcp_dev-7.4.1.dev438 → ha_mcp_dev-7.4.1.dev439}/src/ha_mcp_dev.egg-info/dependency_links.txt
RENAMED
|
File without changes
|
{ha_mcp_dev-7.4.1.dev438 → ha_mcp_dev-7.4.1.dev439}/src/ha_mcp_dev.egg-info/entry_points.txt
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|