ha-mcp-dev 7.4.1.dev415__tar.gz → 7.4.1.dev416__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.dev415/src/ha_mcp_dev.egg-info → ha_mcp_dev-7.4.1.dev416}/PKG-INFO +1 -1
- {ha_mcp_dev-7.4.1.dev415 → ha_mcp_dev-7.4.1.dev416}/pyproject.toml +1 -1
- {ha_mcp_dev-7.4.1.dev415 → ha_mcp_dev-7.4.1.dev416}/src/ha_mcp/tools/tools_addons.py +43 -2
- {ha_mcp_dev-7.4.1.dev415 → ha_mcp_dev-7.4.1.dev416}/src/ha_mcp/tools/tools_integrations.py +42 -2
- {ha_mcp_dev-7.4.1.dev415 → ha_mcp_dev-7.4.1.dev416}/src/ha_mcp/tools/tools_utility.py +109 -5
- {ha_mcp_dev-7.4.1.dev415 → ha_mcp_dev-7.4.1.dev416}/src/ha_mcp/tools/util_helpers.py +81 -0
- {ha_mcp_dev-7.4.1.dev415 → ha_mcp_dev-7.4.1.dev416/src/ha_mcp_dev.egg-info}/PKG-INFO +1 -1
- {ha_mcp_dev-7.4.1.dev415 → ha_mcp_dev-7.4.1.dev416}/LICENSE +0 -0
- {ha_mcp_dev-7.4.1.dev415 → ha_mcp_dev-7.4.1.dev416}/MANIFEST.in +0 -0
- {ha_mcp_dev-7.4.1.dev415 → ha_mcp_dev-7.4.1.dev416}/README.md +0 -0
- {ha_mcp_dev-7.4.1.dev415 → ha_mcp_dev-7.4.1.dev416}/setup.cfg +0 -0
- {ha_mcp_dev-7.4.1.dev415 → ha_mcp_dev-7.4.1.dev416}/src/ha_mcp/__init__.py +0 -0
- {ha_mcp_dev-7.4.1.dev415 → ha_mcp_dev-7.4.1.dev416}/src/ha_mcp/__main__.py +0 -0
- {ha_mcp_dev-7.4.1.dev415 → ha_mcp_dev-7.4.1.dev416}/src/ha_mcp/_pypi_marker +0 -0
- {ha_mcp_dev-7.4.1.dev415 → ha_mcp_dev-7.4.1.dev416}/src/ha_mcp/_version.py +0 -0
- {ha_mcp_dev-7.4.1.dev415 → ha_mcp_dev-7.4.1.dev416}/src/ha_mcp/auth/__init__.py +0 -0
- {ha_mcp_dev-7.4.1.dev415 → ha_mcp_dev-7.4.1.dev416}/src/ha_mcp/auth/consent_form.py +0 -0
- {ha_mcp_dev-7.4.1.dev415 → ha_mcp_dev-7.4.1.dev416}/src/ha_mcp/auth/provider.py +0 -0
- {ha_mcp_dev-7.4.1.dev415 → ha_mcp_dev-7.4.1.dev416}/src/ha_mcp/client/__init__.py +0 -0
- {ha_mcp_dev-7.4.1.dev415 → ha_mcp_dev-7.4.1.dev416}/src/ha_mcp/client/rest_client.py +0 -0
- {ha_mcp_dev-7.4.1.dev415 → ha_mcp_dev-7.4.1.dev416}/src/ha_mcp/client/websocket_client.py +0 -0
- {ha_mcp_dev-7.4.1.dev415 → ha_mcp_dev-7.4.1.dev416}/src/ha_mcp/client/websocket_listener.py +0 -0
- {ha_mcp_dev-7.4.1.dev415 → ha_mcp_dev-7.4.1.dev416}/src/ha_mcp/config.py +0 -0
- {ha_mcp_dev-7.4.1.dev415 → ha_mcp_dev-7.4.1.dev416}/src/ha_mcp/errors.py +0 -0
- {ha_mcp_dev-7.4.1.dev415 → ha_mcp_dev-7.4.1.dev416}/src/ha_mcp/py.typed +0 -0
- {ha_mcp_dev-7.4.1.dev415 → ha_mcp_dev-7.4.1.dev416}/src/ha_mcp/resources/skills-vendor/.claude/settings.json +0 -0
- {ha_mcp_dev-7.4.1.dev415 → ha_mcp_dev-7.4.1.dev416}/src/ha_mcp/resources/skills-vendor/.claude-plugin/marketplace.json +0 -0
- {ha_mcp_dev-7.4.1.dev415 → ha_mcp_dev-7.4.1.dev416}/src/ha_mcp/resources/skills-vendor/.claude-plugin/plugin.json +0 -0
- {ha_mcp_dev-7.4.1.dev415 → ha_mcp_dev-7.4.1.dev416}/src/ha_mcp/resources/skills-vendor/.github/ISSUE_TEMPLATE/skill-rca.md +0 -0
- {ha_mcp_dev-7.4.1.dev415 → ha_mcp_dev-7.4.1.dev416}/src/ha_mcp/resources/skills-vendor/AGENTS.md +0 -0
- {ha_mcp_dev-7.4.1.dev415 → ha_mcp_dev-7.4.1.dev416}/src/ha_mcp/resources/skills-vendor/CLAUDE.md +0 -0
- {ha_mcp_dev-7.4.1.dev415 → ha_mcp_dev-7.4.1.dev416}/src/ha_mcp/resources/skills-vendor/CONTRIBUTING.md +0 -0
- {ha_mcp_dev-7.4.1.dev415 → ha_mcp_dev-7.4.1.dev416}/src/ha_mcp/resources/skills-vendor/LICENSE +0 -0
- {ha_mcp_dev-7.4.1.dev415 → ha_mcp_dev-7.4.1.dev416}/src/ha_mcp/resources/skills-vendor/README.md +0 -0
- {ha_mcp_dev-7.4.1.dev415 → ha_mcp_dev-7.4.1.dev416}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/SKILL.md +0 -0
- {ha_mcp_dev-7.4.1.dev415 → ha_mcp_dev-7.4.1.dev416}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/evals/evals.json +0 -0
- {ha_mcp_dev-7.4.1.dev415 → ha_mcp_dev-7.4.1.dev416}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/automation-patterns.md +0 -0
- {ha_mcp_dev-7.4.1.dev415 → ha_mcp_dev-7.4.1.dev416}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/dashboard-cards.md +0 -0
- {ha_mcp_dev-7.4.1.dev415 → ha_mcp_dev-7.4.1.dev416}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/dashboard-guide.md +0 -0
- {ha_mcp_dev-7.4.1.dev415 → ha_mcp_dev-7.4.1.dev416}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/device-control.md +0 -0
- {ha_mcp_dev-7.4.1.dev415 → ha_mcp_dev-7.4.1.dev416}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/domain-docs.md +0 -0
- {ha_mcp_dev-7.4.1.dev415 → ha_mcp_dev-7.4.1.dev416}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/examples.yaml +0 -0
- {ha_mcp_dev-7.4.1.dev415 → ha_mcp_dev-7.4.1.dev416}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/helper-selection.md +0 -0
- {ha_mcp_dev-7.4.1.dev415 → ha_mcp_dev-7.4.1.dev416}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/safe-refactoring.md +0 -0
- {ha_mcp_dev-7.4.1.dev415 → ha_mcp_dev-7.4.1.dev416}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/template-guidelines.md +0 -0
- {ha_mcp_dev-7.4.1.dev415 → ha_mcp_dev-7.4.1.dev416}/src/ha_mcp/server.py +0 -0
- {ha_mcp_dev-7.4.1.dev415 → ha_mcp_dev-7.4.1.dev416}/src/ha_mcp/settings_ui.py +0 -0
- {ha_mcp_dev-7.4.1.dev415 → ha_mcp_dev-7.4.1.dev416}/src/ha_mcp/smoke_test.py +0 -0
- {ha_mcp_dev-7.4.1.dev415 → ha_mcp_dev-7.4.1.dev416}/src/ha_mcp/tools/__init__.py +0 -0
- {ha_mcp_dev-7.4.1.dev415 → ha_mcp_dev-7.4.1.dev416}/src/ha_mcp/tools/backup.py +0 -0
- {ha_mcp_dev-7.4.1.dev415 → ha_mcp_dev-7.4.1.dev416}/src/ha_mcp/tools/best_practice_checker.py +0 -0
- {ha_mcp_dev-7.4.1.dev415 → ha_mcp_dev-7.4.1.dev416}/src/ha_mcp/tools/device_control.py +0 -0
- {ha_mcp_dev-7.4.1.dev415 → ha_mcp_dev-7.4.1.dev416}/src/ha_mcp/tools/enhanced.py +0 -0
- {ha_mcp_dev-7.4.1.dev415 → ha_mcp_dev-7.4.1.dev416}/src/ha_mcp/tools/helpers.py +0 -0
- {ha_mcp_dev-7.4.1.dev415 → ha_mcp_dev-7.4.1.dev416}/src/ha_mcp/tools/reference_validator.py +0 -0
- {ha_mcp_dev-7.4.1.dev415 → ha_mcp_dev-7.4.1.dev416}/src/ha_mcp/tools/registry.py +0 -0
- {ha_mcp_dev-7.4.1.dev415 → ha_mcp_dev-7.4.1.dev416}/src/ha_mcp/tools/smart_search.py +0 -0
- {ha_mcp_dev-7.4.1.dev415 → ha_mcp_dev-7.4.1.dev416}/src/ha_mcp/tools/tools_areas.py +0 -0
- {ha_mcp_dev-7.4.1.dev415 → ha_mcp_dev-7.4.1.dev416}/src/ha_mcp/tools/tools_blueprints.py +0 -0
- {ha_mcp_dev-7.4.1.dev415 → ha_mcp_dev-7.4.1.dev416}/src/ha_mcp/tools/tools_bug_report.py +0 -0
- {ha_mcp_dev-7.4.1.dev415 → ha_mcp_dev-7.4.1.dev416}/src/ha_mcp/tools/tools_calendar.py +0 -0
- {ha_mcp_dev-7.4.1.dev415 → ha_mcp_dev-7.4.1.dev416}/src/ha_mcp/tools/tools_camera.py +0 -0
- {ha_mcp_dev-7.4.1.dev415 → ha_mcp_dev-7.4.1.dev416}/src/ha_mcp/tools/tools_categories.py +0 -0
- {ha_mcp_dev-7.4.1.dev415 → ha_mcp_dev-7.4.1.dev416}/src/ha_mcp/tools/tools_config_automations.py +0 -0
- {ha_mcp_dev-7.4.1.dev415 → ha_mcp_dev-7.4.1.dev416}/src/ha_mcp/tools/tools_config_dashboards.py +0 -0
- {ha_mcp_dev-7.4.1.dev415 → ha_mcp_dev-7.4.1.dev416}/src/ha_mcp/tools/tools_config_entry_flow.py +0 -0
- {ha_mcp_dev-7.4.1.dev415 → ha_mcp_dev-7.4.1.dev416}/src/ha_mcp/tools/tools_config_helpers.py +0 -0
- {ha_mcp_dev-7.4.1.dev415 → ha_mcp_dev-7.4.1.dev416}/src/ha_mcp/tools/tools_config_scripts.py +0 -0
- {ha_mcp_dev-7.4.1.dev415 → ha_mcp_dev-7.4.1.dev416}/src/ha_mcp/tools/tools_energy.py +0 -0
- {ha_mcp_dev-7.4.1.dev415 → ha_mcp_dev-7.4.1.dev416}/src/ha_mcp/tools/tools_entities.py +0 -0
- {ha_mcp_dev-7.4.1.dev415 → ha_mcp_dev-7.4.1.dev416}/src/ha_mcp/tools/tools_filesystem.py +0 -0
- {ha_mcp_dev-7.4.1.dev415 → ha_mcp_dev-7.4.1.dev416}/src/ha_mcp/tools/tools_groups.py +0 -0
- {ha_mcp_dev-7.4.1.dev415 → ha_mcp_dev-7.4.1.dev416}/src/ha_mcp/tools/tools_hacs.py +0 -0
- {ha_mcp_dev-7.4.1.dev415 → ha_mcp_dev-7.4.1.dev416}/src/ha_mcp/tools/tools_history.py +0 -0
- {ha_mcp_dev-7.4.1.dev415 → ha_mcp_dev-7.4.1.dev416}/src/ha_mcp/tools/tools_labels.py +0 -0
- {ha_mcp_dev-7.4.1.dev415 → ha_mcp_dev-7.4.1.dev416}/src/ha_mcp/tools/tools_mcp_component.py +0 -0
- {ha_mcp_dev-7.4.1.dev415 → ha_mcp_dev-7.4.1.dev416}/src/ha_mcp/tools/tools_registry.py +0 -0
- {ha_mcp_dev-7.4.1.dev415 → ha_mcp_dev-7.4.1.dev416}/src/ha_mcp/tools/tools_resources.py +0 -0
- {ha_mcp_dev-7.4.1.dev415 → ha_mcp_dev-7.4.1.dev416}/src/ha_mcp/tools/tools_search.py +0 -0
- {ha_mcp_dev-7.4.1.dev415 → ha_mcp_dev-7.4.1.dev416}/src/ha_mcp/tools/tools_service.py +0 -0
- {ha_mcp_dev-7.4.1.dev415 → ha_mcp_dev-7.4.1.dev416}/src/ha_mcp/tools/tools_services.py +0 -0
- {ha_mcp_dev-7.4.1.dev415 → ha_mcp_dev-7.4.1.dev416}/src/ha_mcp/tools/tools_system.py +0 -0
- {ha_mcp_dev-7.4.1.dev415 → ha_mcp_dev-7.4.1.dev416}/src/ha_mcp/tools/tools_todo.py +0 -0
- {ha_mcp_dev-7.4.1.dev415 → ha_mcp_dev-7.4.1.dev416}/src/ha_mcp/tools/tools_traces.py +0 -0
- {ha_mcp_dev-7.4.1.dev415 → ha_mcp_dev-7.4.1.dev416}/src/ha_mcp/tools/tools_updates.py +0 -0
- {ha_mcp_dev-7.4.1.dev415 → ha_mcp_dev-7.4.1.dev416}/src/ha_mcp/tools/tools_voice_assistant.py +0 -0
- {ha_mcp_dev-7.4.1.dev415 → ha_mcp_dev-7.4.1.dev416}/src/ha_mcp/tools/tools_yaml_config.py +0 -0
- {ha_mcp_dev-7.4.1.dev415 → ha_mcp_dev-7.4.1.dev416}/src/ha_mcp/tools/tools_zones.py +0 -0
- {ha_mcp_dev-7.4.1.dev415 → ha_mcp_dev-7.4.1.dev416}/src/ha_mcp/transforms/__init__.py +0 -0
- {ha_mcp_dev-7.4.1.dev415 → ha_mcp_dev-7.4.1.dev416}/src/ha_mcp/transforms/categorized_search.py +0 -0
- {ha_mcp_dev-7.4.1.dev415 → ha_mcp_dev-7.4.1.dev416}/src/ha_mcp/utils/__init__.py +0 -0
- {ha_mcp_dev-7.4.1.dev415 → ha_mcp_dev-7.4.1.dev416}/src/ha_mcp/utils/config_hash.py +0 -0
- {ha_mcp_dev-7.4.1.dev415 → ha_mcp_dev-7.4.1.dev416}/src/ha_mcp/utils/domain_handlers.py +0 -0
- {ha_mcp_dev-7.4.1.dev415 → ha_mcp_dev-7.4.1.dev416}/src/ha_mcp/utils/fuzzy_search.py +0 -0
- {ha_mcp_dev-7.4.1.dev415 → ha_mcp_dev-7.4.1.dev416}/src/ha_mcp/utils/operation_manager.py +0 -0
- {ha_mcp_dev-7.4.1.dev415 → ha_mcp_dev-7.4.1.dev416}/src/ha_mcp/utils/python_sandbox.py +0 -0
- {ha_mcp_dev-7.4.1.dev415 → ha_mcp_dev-7.4.1.dev416}/src/ha_mcp/utils/usage_logger.py +0 -0
- {ha_mcp_dev-7.4.1.dev415 → ha_mcp_dev-7.4.1.dev416}/src/ha_mcp_dev.egg-info/SOURCES.txt +0 -0
- {ha_mcp_dev-7.4.1.dev415 → ha_mcp_dev-7.4.1.dev416}/src/ha_mcp_dev.egg-info/dependency_links.txt +0 -0
- {ha_mcp_dev-7.4.1.dev415 → ha_mcp_dev-7.4.1.dev416}/src/ha_mcp_dev.egg-info/entry_points.txt +0 -0
- {ha_mcp_dev-7.4.1.dev415 → ha_mcp_dev-7.4.1.dev416}/src/ha_mcp_dev.egg-info/requires.txt +0 -0
- {ha_mcp_dev-7.4.1.dev415 → ha_mcp_dev-7.4.1.dev416}/src/ha_mcp_dev.egg-info/top_level.txt +0 -0
- {ha_mcp_dev-7.4.1.dev415 → ha_mcp_dev-7.4.1.dev416}/tests/__init__.py +0 -0
- {ha_mcp_dev-7.4.1.dev415 → ha_mcp_dev-7.4.1.dev416}/tests/test_constants.py +0 -0
- {ha_mcp_dev-7.4.1.dev415 → ha_mcp_dev-7.4.1.dev416}/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.dev416"
|
|
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"
|
|
@@ -295,11 +295,50 @@ async def get_addon_info(client: HomeAssistantClient, slug: str) -> dict[str, An
|
|
|
295
295
|
|
|
296
296
|
Returns:
|
|
297
297
|
Dictionary with add-on details including ingress info, state, options, etc.
|
|
298
|
+
Top-level ``log_level`` is surfaced when the add-on exposes one via its
|
|
299
|
+
Supervisor options or schema (e.g., ``"debug"``, ``"info"``, etc.).
|
|
298
300
|
"""
|
|
299
301
|
response = await _supervisor_api_call(client, f"/addons/{slug}/info")
|
|
300
302
|
if not response.get("success"):
|
|
301
303
|
return response # TODO(tech-debt): should raise ToolError per AGENTS.md Pattern B
|
|
302
|
-
|
|
304
|
+
|
|
305
|
+
addon = response["result"] if isinstance(response["result"], dict) else {}
|
|
306
|
+
result: dict[str, Any] = {"success": True, "addon": addon}
|
|
307
|
+
|
|
308
|
+
log_level = _extract_addon_log_level(addon)
|
|
309
|
+
if log_level is not None:
|
|
310
|
+
result["log_level"] = log_level
|
|
311
|
+
|
|
312
|
+
return result
|
|
313
|
+
|
|
314
|
+
|
|
315
|
+
def _extract_addon_log_level(addon: dict[str, Any]) -> str | None:
|
|
316
|
+
"""Return the add-on's configured log level, if any.
|
|
317
|
+
|
|
318
|
+
Checks the add-on's current options first (``options.log_level`` — what the
|
|
319
|
+
user set), then falls back to the schema (Supervisor serializes ``schema``
|
|
320
|
+
as a list of ``{name, type, ...}`` field descriptors) so add-ons that ship a
|
|
321
|
+
log_level option without a value still surface ``"default"``. Returns
|
|
322
|
+
``None`` when the add-on exposes no log_level option at all.
|
|
323
|
+
|
|
324
|
+
The lower-case ``"default"`` is the literal Supervisor sentinel; the
|
|
325
|
+
integration path uses ``"DEFAULT"`` (uppercase) — these are distinct values
|
|
326
|
+
by design and should not be cross-compared.
|
|
327
|
+
"""
|
|
328
|
+
options = addon.get("options")
|
|
329
|
+
if isinstance(options, dict):
|
|
330
|
+
level = options.get("log_level")
|
|
331
|
+
if isinstance(level, str) and level.strip():
|
|
332
|
+
return level
|
|
333
|
+
|
|
334
|
+
schema = addon.get("schema")
|
|
335
|
+
if isinstance(schema, list) and any(
|
|
336
|
+
isinstance(item, dict) and item.get("name") == "log_level"
|
|
337
|
+
for item in schema
|
|
338
|
+
):
|
|
339
|
+
return "default"
|
|
340
|
+
|
|
341
|
+
return None
|
|
303
342
|
|
|
304
343
|
|
|
305
344
|
async def list_addons(
|
|
@@ -1153,7 +1192,9 @@ def register_addon_tools(mcp: Any, client: HomeAssistantClient, **kwargs: Any) -
|
|
|
1153
1192
|
**Note:** This tool only works with Home Assistant OS or Supervised installations.
|
|
1154
1193
|
|
|
1155
1194
|
**SINGLE ADD-ON (slug provided):**
|
|
1156
|
-
Returns comprehensive details including ingress entry, ports, options,
|
|
1195
|
+
Returns comprehensive details including ingress entry, ports, options, state,
|
|
1196
|
+
and (when the add-on exposes one) a top-level ``log_level`` reflecting the
|
|
1197
|
+
current Supervisor option — useful for confirming ha_manage_addon log_level changes.
|
|
1157
1198
|
Useful for discovering what APIs an add-on exposes before calling ha_manage_addon.
|
|
1158
1199
|
|
|
1159
1200
|
**INSTALLED ADD-ONS (source='installed'):**
|
|
@@ -34,6 +34,7 @@ from .util_helpers import (
|
|
|
34
34
|
build_pagination_metadata,
|
|
35
35
|
coerce_bool_param,
|
|
36
36
|
coerce_int_param,
|
|
37
|
+
get_logger_levels,
|
|
37
38
|
wait_for_entity_removed,
|
|
38
39
|
)
|
|
39
40
|
|
|
@@ -233,6 +234,19 @@ class IntegrationTools:
|
|
|
233
234
|
|
|
234
235
|
STATES: 'loaded', 'setup_error', 'setup_retry', 'not_loaded',
|
|
235
236
|
'failed_unload', 'migration_error'.
|
|
237
|
+
|
|
238
|
+
Each entry carries:
|
|
239
|
+
|
|
240
|
+
- ``log_level``: the canonical Python logger level name
|
|
241
|
+
(``DEBUG``/``INFO``/``WARNING``/``ERROR``/``CRITICAL``) when the
|
|
242
|
+
integration has a ``logger.set_level`` override, or ``"DEFAULT"``
|
|
243
|
+
(uppercase sentinel) when no override is set.
|
|
244
|
+
- ``log_level_raw``: the original numeric level (e.g. ``10`` for DEBUG)
|
|
245
|
+
when HA returned an int, ``None`` otherwise (no override set, or HA
|
|
246
|
+
provided a level name as a string).
|
|
247
|
+
|
|
248
|
+
This is distinct from the add-on side, where ``ha_get_addon`` returns
|
|
249
|
+
Supervisor's lowercase ``"default"`` literal — do not cross-compare.
|
|
236
250
|
"""
|
|
237
251
|
try:
|
|
238
252
|
include_opts = coerce_bool_param(
|
|
@@ -280,12 +294,21 @@ class IntegrationTools:
|
|
|
280
294
|
"""Fetch a single config entry by ID, optionally including its options schema."""
|
|
281
295
|
try:
|
|
282
296
|
result = await self._client.get_config_entry(entry_id)
|
|
297
|
+
entry_domain = result.get("domain") if isinstance(result, dict) else None
|
|
283
298
|
resp: dict[str, Any] = {
|
|
284
299
|
"success": True,
|
|
285
300
|
"entry_id": entry_id,
|
|
286
301
|
"entry": result,
|
|
287
302
|
}
|
|
288
303
|
|
|
304
|
+
# Surface the effective Python logger level for this integration
|
|
305
|
+
# so users can confirm logger.set_level changes took effect.
|
|
306
|
+
# Emit unconditionally for symmetry with the list path (_format_entry).
|
|
307
|
+
logger_levels = await get_logger_levels(self._client)
|
|
308
|
+
level_info = logger_levels.get(entry_domain or "")
|
|
309
|
+
resp["log_level"] = level_info["name"] if level_info else "DEFAULT"
|
|
310
|
+
resp["log_level_raw"] = level_info["raw"] if level_info else None
|
|
311
|
+
|
|
289
312
|
# Optionally fetch options flow schema (logically read-only: start+abort)
|
|
290
313
|
if include_schema and result.get("supports_options"):
|
|
291
314
|
await self._fetch_options_schema(entry_id, resp)
|
|
@@ -368,9 +391,12 @@ class IntegrationTools:
|
|
|
368
391
|
e for e in entries if e.get("domain", "").lower() == domain_lower
|
|
369
392
|
]
|
|
370
393
|
|
|
394
|
+
# Fetch current logger levels once; enrich each entry with its effective level.
|
|
395
|
+
logger_levels = await get_logger_levels(self._client)
|
|
396
|
+
|
|
371
397
|
# Format entries for response
|
|
372
398
|
formatted_entries = [
|
|
373
|
-
self._format_entry(entry, include_opts) for entry in entries
|
|
399
|
+
self._format_entry(entry, include_opts, logger_levels) for entry in entries
|
|
374
400
|
]
|
|
375
401
|
|
|
376
402
|
# Apply search filter if query provided
|
|
@@ -403,7 +429,11 @@ class IntegrationTools:
|
|
|
403
429
|
return result_data
|
|
404
430
|
|
|
405
431
|
@staticmethod
|
|
406
|
-
def _format_entry(
|
|
432
|
+
def _format_entry(
|
|
433
|
+
entry: dict[str, Any],
|
|
434
|
+
include_opts: bool | None,
|
|
435
|
+
logger_levels: dict[str, dict[str, Any]] | None = None,
|
|
436
|
+
) -> dict[str, Any]:
|
|
407
437
|
"""Format a raw config entry into the response shape."""
|
|
408
438
|
formatted_entry: dict[str, Any] = {
|
|
409
439
|
"entry_id": entry.get("entry_id"),
|
|
@@ -416,6 +446,16 @@ class IntegrationTools:
|
|
|
416
446
|
"disabled_by": entry.get("disabled_by"),
|
|
417
447
|
}
|
|
418
448
|
|
|
449
|
+
# Surface the effective Python logger level for this integration
|
|
450
|
+
# ("DEFAULT" = no override; falls back to the root logger level).
|
|
451
|
+
# `log_level_raw` is the original numeric level (None when no override
|
|
452
|
+
# exists or HA returned a string instead of an int).
|
|
453
|
+
if logger_levels is not None:
|
|
454
|
+
domain = entry.get("domain") or ""
|
|
455
|
+
level_info = logger_levels.get(domain)
|
|
456
|
+
formatted_entry["log_level"] = level_info["name"] if level_info else "DEFAULT"
|
|
457
|
+
formatted_entry["log_level_raw"] = level_info["raw"] if level_info else None
|
|
458
|
+
|
|
419
459
|
# Include options when requested (for auditing template definitions, etc.)
|
|
420
460
|
if include_opts:
|
|
421
461
|
formatted_entry["options"] = entry.get("options", {})
|
|
@@ -15,7 +15,12 @@ from fastmcp.exceptions import ToolError
|
|
|
15
15
|
from ..client.rest_client import HomeAssistantAPIError, HomeAssistantConnectionError
|
|
16
16
|
from ..errors import ErrorCode, create_error_response
|
|
17
17
|
from .helpers import exception_to_structured_error, log_tool_usage, raise_tool_error
|
|
18
|
-
from .util_helpers import
|
|
18
|
+
from .util_helpers import (
|
|
19
|
+
add_timezone_metadata,
|
|
20
|
+
coerce_bool_param,
|
|
21
|
+
coerce_int_param,
|
|
22
|
+
normalize_log_level,
|
|
23
|
+
)
|
|
19
24
|
|
|
20
25
|
logger = logging.getLogger(__name__)
|
|
21
26
|
|
|
@@ -79,7 +84,7 @@ def register_utility_tools(mcp: Any, client: Any, **kwargs: Any) -> None:
|
|
|
79
84
|
)
|
|
80
85
|
@log_tool_usage
|
|
81
86
|
async def ha_get_logs(
|
|
82
|
-
source: Literal["logbook", "system", "error_log", "supervisor"] = "logbook",
|
|
87
|
+
source: Literal["logbook", "system", "error_log", "supervisor", "logger"] = "logbook",
|
|
83
88
|
# Shared parameters
|
|
84
89
|
limit: int | str | None = None,
|
|
85
90
|
search: str | None = None,
|
|
@@ -102,8 +107,9 @@ def register_utility_tools(mcp: Any, client: Any, **kwargs: Any) -> None:
|
|
|
102
107
|
- "system": Structured system log entries (errors, warnings) via system_log/list
|
|
103
108
|
- "error_log": Raw home-assistant.log text
|
|
104
109
|
- "supervisor": Add-on container logs (requires slug parameter)
|
|
110
|
+
- "logger": Effective log level per integration via logger/log_info (confirms logger.set_level changes took effect)
|
|
105
111
|
|
|
106
|
-
**Shared params:** limit, search (keyword filter on entries/lines)
|
|
112
|
+
**Shared params:** limit, search (keyword filter on entries/lines; matches integration domain for source='logger')
|
|
107
113
|
**Logbook params:** hours_back, entity_id, end_time, offset, compact (default True — strips attribute dicts to save context)
|
|
108
114
|
**System/error_log params:** level (ERROR, WARNING, INFO, DEBUG)
|
|
109
115
|
**Supervisor params:** slug (add-on slug, e.g. "core_mosquitto")
|
|
@@ -130,10 +136,10 @@ def register_utility_tools(mcp: Any, client: Any, **kwargs: Any) -> None:
|
|
|
130
136
|
f"Parameters {', '.join(ignored)} only apply to source='logbook'; "
|
|
131
137
|
f"ignored for source='{source}'"
|
|
132
138
|
)
|
|
133
|
-
if source
|
|
139
|
+
if source in ("logbook", "logger", "supervisor") and level is not None:
|
|
134
140
|
warnings.append(
|
|
135
141
|
"Parameter 'level' only applies to source='system' or 'error_log'; "
|
|
136
|
-
"ignored for source='
|
|
142
|
+
f"ignored for source='{source}'"
|
|
137
143
|
)
|
|
138
144
|
|
|
139
145
|
# --- source="logbook" ---
|
|
@@ -173,6 +179,13 @@ def register_utility_tools(mcp: Any, client: Any, **kwargs: Any) -> None:
|
|
|
173
179
|
result["warnings"] = warnings
|
|
174
180
|
return result
|
|
175
181
|
|
|
182
|
+
# --- source="logger" ---
|
|
183
|
+
if source == "logger":
|
|
184
|
+
result = await _get_logger_info(limit=limit, search=search)
|
|
185
|
+
if warnings:
|
|
186
|
+
result["warnings"] = warnings
|
|
187
|
+
return result
|
|
188
|
+
|
|
176
189
|
# --- source="supervisor" ---
|
|
177
190
|
# source == "supervisor" (Literal type guarantees this)
|
|
178
191
|
if not slug:
|
|
@@ -508,6 +521,97 @@ def register_utility_tools(mcp: Any, client: Any, **kwargs: Any) -> None:
|
|
|
508
521
|
],
|
|
509
522
|
)
|
|
510
523
|
|
|
524
|
+
# ---- Logger info source ----
|
|
525
|
+
|
|
526
|
+
async def _get_logger_info(
|
|
527
|
+
limit: int | str | None = None,
|
|
528
|
+
search: str | None = None,
|
|
529
|
+
) -> dict[str, Any]:
|
|
530
|
+
"""Fetch per-integration log levels via the ``logger/log_info`` WS command."""
|
|
531
|
+
effective_limit = _coerce_limit(limit)
|
|
532
|
+
|
|
533
|
+
try:
|
|
534
|
+
result = await client.send_websocket_message({"type": "logger/log_info"})
|
|
535
|
+
|
|
536
|
+
if not result.get("success"):
|
|
537
|
+
raise_tool_error(
|
|
538
|
+
create_error_response(
|
|
539
|
+
ErrorCode.SERVICE_CALL_FAILED,
|
|
540
|
+
result.get("error", "Failed to retrieve logger info"),
|
|
541
|
+
suggestions=[
|
|
542
|
+
"Verify the 'logger' integration is enabled in Home Assistant",
|
|
543
|
+
"Check Home Assistant WebSocket connection",
|
|
544
|
+
],
|
|
545
|
+
)
|
|
546
|
+
)
|
|
547
|
+
|
|
548
|
+
raw_entries = result.get("result", [])
|
|
549
|
+
if not isinstance(raw_entries, list):
|
|
550
|
+
raw_entries = []
|
|
551
|
+
|
|
552
|
+
loggers: list[dict[str, Any]] = []
|
|
553
|
+
for entry in raw_entries:
|
|
554
|
+
if not isinstance(entry, dict):
|
|
555
|
+
continue
|
|
556
|
+
domain = entry.get("domain")
|
|
557
|
+
if not isinstance(domain, str) or not domain:
|
|
558
|
+
continue
|
|
559
|
+
raw_level = entry.get("level")
|
|
560
|
+
level_name = normalize_log_level(raw_level)
|
|
561
|
+
if level_name is None:
|
|
562
|
+
continue
|
|
563
|
+
loggers.append(
|
|
564
|
+
{
|
|
565
|
+
"domain": domain,
|
|
566
|
+
"level": level_name,
|
|
567
|
+
"level_raw": raw_level if isinstance(raw_level, int) else None,
|
|
568
|
+
}
|
|
569
|
+
)
|
|
570
|
+
|
|
571
|
+
filters_applied: dict[str, str] = {}
|
|
572
|
+
if search:
|
|
573
|
+
search_lower = search.lower()
|
|
574
|
+
loggers = [
|
|
575
|
+
entry for entry in loggers
|
|
576
|
+
if search_lower in entry["domain"].lower()
|
|
577
|
+
]
|
|
578
|
+
filters_applied["search"] = search
|
|
579
|
+
|
|
580
|
+
loggers.sort(key=lambda entry: entry["domain"])
|
|
581
|
+
|
|
582
|
+
total_entries = len(loggers)
|
|
583
|
+
loggers = loggers[:effective_limit]
|
|
584
|
+
|
|
585
|
+
data: dict[str, Any] = {
|
|
586
|
+
"success": True,
|
|
587
|
+
"source": "logger",
|
|
588
|
+
"loggers": loggers,
|
|
589
|
+
"total_entries": total_entries,
|
|
590
|
+
"returned_entries": len(loggers),
|
|
591
|
+
"limit": effective_limit,
|
|
592
|
+
}
|
|
593
|
+
if filters_applied:
|
|
594
|
+
data["filters_applied"] = filters_applied
|
|
595
|
+
|
|
596
|
+
return data
|
|
597
|
+
|
|
598
|
+
except ToolError:
|
|
599
|
+
raise
|
|
600
|
+
except (
|
|
601
|
+
HomeAssistantConnectionError,
|
|
602
|
+
HomeAssistantAPIError,
|
|
603
|
+
TimeoutError,
|
|
604
|
+
OSError,
|
|
605
|
+
) as e:
|
|
606
|
+
exception_to_structured_error(
|
|
607
|
+
e,
|
|
608
|
+
context={"source": "logger"},
|
|
609
|
+
suggestions=[
|
|
610
|
+
"Check Home Assistant WebSocket connection",
|
|
611
|
+
"Verify the 'logger' integration is enabled",
|
|
612
|
+
],
|
|
613
|
+
)
|
|
614
|
+
|
|
511
615
|
# ---- Supervisor log source ----
|
|
512
616
|
|
|
513
617
|
async def _get_supervisor_log(
|
|
@@ -268,6 +268,87 @@ def unwrap_service_response(result: dict[str, Any]) -> dict[str, Any]:
|
|
|
268
268
|
return sr if isinstance(sr, dict) else result
|
|
269
269
|
|
|
270
270
|
|
|
271
|
+
# Python logging numeric-level → canonical level name.
|
|
272
|
+
# Mirrors the values in HA's LOGSEVERITY constant (components/logger/const.py).
|
|
273
|
+
_LOG_LEVEL_NAMES: dict[int, str] = {
|
|
274
|
+
0: "NOTSET",
|
|
275
|
+
10: "DEBUG",
|
|
276
|
+
20: "INFO",
|
|
277
|
+
30: "WARNING",
|
|
278
|
+
40: "ERROR",
|
|
279
|
+
50: "CRITICAL",
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
|
|
283
|
+
def normalize_log_level(level: Any) -> str | None:
|
|
284
|
+
"""Normalize a numeric or string log level to its canonical uppercase name.
|
|
285
|
+
|
|
286
|
+
Returns None if the value can't be recognized as a log level.
|
|
287
|
+
"""
|
|
288
|
+
if isinstance(level, bool): # bool is an int subclass — reject explicitly
|
|
289
|
+
return None
|
|
290
|
+
if isinstance(level, int):
|
|
291
|
+
return _LOG_LEVEL_NAMES.get(level, f"LEVEL_{level}")
|
|
292
|
+
if isinstance(level, str):
|
|
293
|
+
stripped = level.strip().upper()
|
|
294
|
+
if not stripped:
|
|
295
|
+
return None
|
|
296
|
+
return stripped
|
|
297
|
+
return None
|
|
298
|
+
|
|
299
|
+
|
|
300
|
+
async def get_logger_levels(client: Any) -> dict[str, dict[str, Any]]:
|
|
301
|
+
"""Fetch current HA integration log levels via the ``logger/log_info`` WS command.
|
|
302
|
+
|
|
303
|
+
Returns a mapping of integration domain (e.g. ``"mqtt"``) to a dict with:
|
|
304
|
+
|
|
305
|
+
- ``name``: canonical level name (``"DEBUG"``, ``"INFO"``, ``"WARNING"``,
|
|
306
|
+
``"ERROR"``, ``"CRITICAL"``, ``"NOTSET"``, or ``"LEVEL_<n>"`` for
|
|
307
|
+
non-standard ints).
|
|
308
|
+
- ``raw``: the original numeric level (``int``) when HA returned an int,
|
|
309
|
+
otherwise ``None`` (e.g. when the level was already provided as a string).
|
|
310
|
+
|
|
311
|
+
Best-effort enrichment: returns an empty dict on connection/IO failures so
|
|
312
|
+
callers can treat it as "no custom levels". Programming errors are not
|
|
313
|
+
suppressed — they surface as bugs during development/CI.
|
|
314
|
+
"""
|
|
315
|
+
try:
|
|
316
|
+
result = await client.send_websocket_message({"type": "logger/log_info"})
|
|
317
|
+
except (
|
|
318
|
+
HomeAssistantConnectionError,
|
|
319
|
+
HomeAssistantAPIError,
|
|
320
|
+
HomeAssistantAuthError,
|
|
321
|
+
TimeoutError,
|
|
322
|
+
OSError,
|
|
323
|
+
) as exc:
|
|
324
|
+
logger.debug("logger/log_info fetch failed: %s", exc)
|
|
325
|
+
return {}
|
|
326
|
+
|
|
327
|
+
if not isinstance(result, dict) or not result.get("success"):
|
|
328
|
+
return {}
|
|
329
|
+
|
|
330
|
+
entries = result.get("result", [])
|
|
331
|
+
if not isinstance(entries, list):
|
|
332
|
+
return {}
|
|
333
|
+
|
|
334
|
+
levels: dict[str, dict[str, Any]] = {}
|
|
335
|
+
for entry in entries:
|
|
336
|
+
if not isinstance(entry, dict):
|
|
337
|
+
continue
|
|
338
|
+
domain = entry.get("domain")
|
|
339
|
+
if not isinstance(domain, str) or not domain:
|
|
340
|
+
continue
|
|
341
|
+
raw_level = entry.get("level")
|
|
342
|
+
name = normalize_log_level(raw_level)
|
|
343
|
+
if name is None:
|
|
344
|
+
continue
|
|
345
|
+
levels[domain] = {
|
|
346
|
+
"name": name,
|
|
347
|
+
"raw": raw_level if isinstance(raw_level, int) and not isinstance(raw_level, bool) else None,
|
|
348
|
+
}
|
|
349
|
+
return levels
|
|
350
|
+
|
|
351
|
+
|
|
271
352
|
async def add_timezone_metadata(client: Any, data: dict[str, Any]) -> dict[str, Any]:
|
|
272
353
|
"""Add Home Assistant timezone to tool responses for local time context."""
|
|
273
354
|
try:
|
|
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.dev415 → ha_mcp_dev-7.4.1.dev416}/src/ha_mcp/resources/skills-vendor/AGENTS.md
RENAMED
|
File without changes
|
{ha_mcp_dev-7.4.1.dev415 → ha_mcp_dev-7.4.1.dev416}/src/ha_mcp/resources/skills-vendor/CLAUDE.md
RENAMED
|
File without changes
|
|
File without changes
|
{ha_mcp_dev-7.4.1.dev415 → ha_mcp_dev-7.4.1.dev416}/src/ha_mcp/resources/skills-vendor/LICENSE
RENAMED
|
File without changes
|
{ha_mcp_dev-7.4.1.dev415 → ha_mcp_dev-7.4.1.dev416}/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
|
|
File without changes
|
{ha_mcp_dev-7.4.1.dev415 → ha_mcp_dev-7.4.1.dev416}/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
|
{ha_mcp_dev-7.4.1.dev415 → ha_mcp_dev-7.4.1.dev416}/src/ha_mcp/tools/tools_config_automations.py
RENAMED
|
File without changes
|
{ha_mcp_dev-7.4.1.dev415 → ha_mcp_dev-7.4.1.dev416}/src/ha_mcp/tools/tools_config_dashboards.py
RENAMED
|
File without changes
|
{ha_mcp_dev-7.4.1.dev415 → ha_mcp_dev-7.4.1.dev416}/src/ha_mcp/tools/tools_config_entry_flow.py
RENAMED
|
File without changes
|
{ha_mcp_dev-7.4.1.dev415 → ha_mcp_dev-7.4.1.dev416}/src/ha_mcp/tools/tools_config_helpers.py
RENAMED
|
File without changes
|
{ha_mcp_dev-7.4.1.dev415 → ha_mcp_dev-7.4.1.dev416}/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
|
{ha_mcp_dev-7.4.1.dev415 → ha_mcp_dev-7.4.1.dev416}/src/ha_mcp/tools/tools_voice_assistant.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{ha_mcp_dev-7.4.1.dev415 → ha_mcp_dev-7.4.1.dev416}/src/ha_mcp/transforms/categorized_search.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
|
{ha_mcp_dev-7.4.1.dev415 → ha_mcp_dev-7.4.1.dev416}/src/ha_mcp_dev.egg-info/dependency_links.txt
RENAMED
|
File without changes
|
{ha_mcp_dev-7.4.1.dev415 → ha_mcp_dev-7.4.1.dev416}/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
|