ha-mcp-dev 6.7.2.dev250__tar.gz → 6.7.2.dev251__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-6.7.2.dev250/src/ha_mcp_dev.egg-info → ha_mcp_dev-6.7.2.dev251}/PKG-INFO +1 -1
- {ha_mcp_dev-6.7.2.dev250 → ha_mcp_dev-6.7.2.dev251}/pyproject.toml +1 -1
- {ha_mcp_dev-6.7.2.dev250 → ha_mcp_dev-6.7.2.dev251}/src/ha_mcp/tools/tools_updates.py +230 -2
- {ha_mcp_dev-6.7.2.dev250 → ha_mcp_dev-6.7.2.dev251/src/ha_mcp_dev.egg-info}/PKG-INFO +1 -1
- {ha_mcp_dev-6.7.2.dev250 → ha_mcp_dev-6.7.2.dev251}/LICENSE +0 -0
- {ha_mcp_dev-6.7.2.dev250 → ha_mcp_dev-6.7.2.dev251}/MANIFEST.in +0 -0
- {ha_mcp_dev-6.7.2.dev250 → ha_mcp_dev-6.7.2.dev251}/README.md +0 -0
- {ha_mcp_dev-6.7.2.dev250 → ha_mcp_dev-6.7.2.dev251}/setup.cfg +0 -0
- {ha_mcp_dev-6.7.2.dev250 → ha_mcp_dev-6.7.2.dev251}/src/ha_mcp/__init__.py +0 -0
- {ha_mcp_dev-6.7.2.dev250 → ha_mcp_dev-6.7.2.dev251}/src/ha_mcp/__main__.py +0 -0
- {ha_mcp_dev-6.7.2.dev250 → ha_mcp_dev-6.7.2.dev251}/src/ha_mcp/_pypi_marker +0 -0
- {ha_mcp_dev-6.7.2.dev250 → ha_mcp_dev-6.7.2.dev251}/src/ha_mcp/auth/__init__.py +0 -0
- {ha_mcp_dev-6.7.2.dev250 → ha_mcp_dev-6.7.2.dev251}/src/ha_mcp/auth/consent_form.py +0 -0
- {ha_mcp_dev-6.7.2.dev250 → ha_mcp_dev-6.7.2.dev251}/src/ha_mcp/auth/provider.py +0 -0
- {ha_mcp_dev-6.7.2.dev250 → ha_mcp_dev-6.7.2.dev251}/src/ha_mcp/client/__init__.py +0 -0
- {ha_mcp_dev-6.7.2.dev250 → ha_mcp_dev-6.7.2.dev251}/src/ha_mcp/client/rest_client.py +0 -0
- {ha_mcp_dev-6.7.2.dev250 → ha_mcp_dev-6.7.2.dev251}/src/ha_mcp/client/websocket_client.py +0 -0
- {ha_mcp_dev-6.7.2.dev250 → ha_mcp_dev-6.7.2.dev251}/src/ha_mcp/client/websocket_listener.py +0 -0
- {ha_mcp_dev-6.7.2.dev250 → ha_mcp_dev-6.7.2.dev251}/src/ha_mcp/config.py +0 -0
- {ha_mcp_dev-6.7.2.dev250 → ha_mcp_dev-6.7.2.dev251}/src/ha_mcp/errors.py +0 -0
- {ha_mcp_dev-6.7.2.dev250 → ha_mcp_dev-6.7.2.dev251}/src/ha_mcp/py.typed +0 -0
- {ha_mcp_dev-6.7.2.dev250 → ha_mcp_dev-6.7.2.dev251}/src/ha_mcp/resources/card_types.json +0 -0
- {ha_mcp_dev-6.7.2.dev250 → ha_mcp_dev-6.7.2.dev251}/src/ha_mcp/resources/dashboard_guide.md +0 -0
- {ha_mcp_dev-6.7.2.dev250 → ha_mcp_dev-6.7.2.dev251}/src/ha_mcp/resources/skills-vendor/.claude/settings.json +0 -0
- {ha_mcp_dev-6.7.2.dev250 → ha_mcp_dev-6.7.2.dev251}/src/ha_mcp/resources/skills-vendor/.claude-plugin/marketplace.json +0 -0
- {ha_mcp_dev-6.7.2.dev250 → ha_mcp_dev-6.7.2.dev251}/src/ha_mcp/resources/skills-vendor/.claude-plugin/plugin.json +0 -0
- {ha_mcp_dev-6.7.2.dev250 → ha_mcp_dev-6.7.2.dev251}/src/ha_mcp/resources/skills-vendor/.github/ISSUE_TEMPLATE/skill-rca.md +0 -0
- {ha_mcp_dev-6.7.2.dev250 → ha_mcp_dev-6.7.2.dev251}/src/ha_mcp/resources/skills-vendor/AGENTS.md +0 -0
- {ha_mcp_dev-6.7.2.dev250 → ha_mcp_dev-6.7.2.dev251}/src/ha_mcp/resources/skills-vendor/CLAUDE.md +0 -0
- {ha_mcp_dev-6.7.2.dev250 → ha_mcp_dev-6.7.2.dev251}/src/ha_mcp/resources/skills-vendor/CONTRIBUTING.md +0 -0
- {ha_mcp_dev-6.7.2.dev250 → ha_mcp_dev-6.7.2.dev251}/src/ha_mcp/resources/skills-vendor/LICENSE +0 -0
- {ha_mcp_dev-6.7.2.dev250 → ha_mcp_dev-6.7.2.dev251}/src/ha_mcp/resources/skills-vendor/README.md +0 -0
- {ha_mcp_dev-6.7.2.dev250 → ha_mcp_dev-6.7.2.dev251}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/SKILL.md +0 -0
- {ha_mcp_dev-6.7.2.dev250 → ha_mcp_dev-6.7.2.dev251}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/automation-patterns.md +0 -0
- {ha_mcp_dev-6.7.2.dev250 → ha_mcp_dev-6.7.2.dev251}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/device-control.md +0 -0
- {ha_mcp_dev-6.7.2.dev250 → ha_mcp_dev-6.7.2.dev251}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/examples.yaml +0 -0
- {ha_mcp_dev-6.7.2.dev250 → ha_mcp_dev-6.7.2.dev251}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/helper-selection.md +0 -0
- {ha_mcp_dev-6.7.2.dev250 → ha_mcp_dev-6.7.2.dev251}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/safe-refactoring.md +0 -0
- {ha_mcp_dev-6.7.2.dev250 → ha_mcp_dev-6.7.2.dev251}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/template-guidelines.md +0 -0
- {ha_mcp_dev-6.7.2.dev250 → ha_mcp_dev-6.7.2.dev251}/src/ha_mcp/server.py +0 -0
- {ha_mcp_dev-6.7.2.dev250 → ha_mcp_dev-6.7.2.dev251}/src/ha_mcp/smoke_test.py +0 -0
- {ha_mcp_dev-6.7.2.dev250 → ha_mcp_dev-6.7.2.dev251}/src/ha_mcp/tools/__init__.py +0 -0
- {ha_mcp_dev-6.7.2.dev250 → ha_mcp_dev-6.7.2.dev251}/src/ha_mcp/tools/backup.py +0 -0
- {ha_mcp_dev-6.7.2.dev250 → ha_mcp_dev-6.7.2.dev251}/src/ha_mcp/tools/device_control.py +0 -0
- {ha_mcp_dev-6.7.2.dev250 → ha_mcp_dev-6.7.2.dev251}/src/ha_mcp/tools/enhanced.py +0 -0
- {ha_mcp_dev-6.7.2.dev250 → ha_mcp_dev-6.7.2.dev251}/src/ha_mcp/tools/helpers.py +0 -0
- {ha_mcp_dev-6.7.2.dev250 → ha_mcp_dev-6.7.2.dev251}/src/ha_mcp/tools/registry.py +0 -0
- {ha_mcp_dev-6.7.2.dev250 → ha_mcp_dev-6.7.2.dev251}/src/ha_mcp/tools/smart_search.py +0 -0
- {ha_mcp_dev-6.7.2.dev250 → ha_mcp_dev-6.7.2.dev251}/src/ha_mcp/tools/tools_addons.py +0 -0
- {ha_mcp_dev-6.7.2.dev250 → ha_mcp_dev-6.7.2.dev251}/src/ha_mcp/tools/tools_areas.py +0 -0
- {ha_mcp_dev-6.7.2.dev250 → ha_mcp_dev-6.7.2.dev251}/src/ha_mcp/tools/tools_blueprints.py +0 -0
- {ha_mcp_dev-6.7.2.dev250 → ha_mcp_dev-6.7.2.dev251}/src/ha_mcp/tools/tools_bug_report.py +0 -0
- {ha_mcp_dev-6.7.2.dev250 → ha_mcp_dev-6.7.2.dev251}/src/ha_mcp/tools/tools_calendar.py +0 -0
- {ha_mcp_dev-6.7.2.dev250 → ha_mcp_dev-6.7.2.dev251}/src/ha_mcp/tools/tools_camera.py +0 -0
- {ha_mcp_dev-6.7.2.dev250 → ha_mcp_dev-6.7.2.dev251}/src/ha_mcp/tools/tools_config_automations.py +0 -0
- {ha_mcp_dev-6.7.2.dev250 → ha_mcp_dev-6.7.2.dev251}/src/ha_mcp/tools/tools_config_dashboards.py +0 -0
- {ha_mcp_dev-6.7.2.dev250 → ha_mcp_dev-6.7.2.dev251}/src/ha_mcp/tools/tools_config_entry_flow.py +0 -0
- {ha_mcp_dev-6.7.2.dev250 → ha_mcp_dev-6.7.2.dev251}/src/ha_mcp/tools/tools_config_helpers.py +0 -0
- {ha_mcp_dev-6.7.2.dev250 → ha_mcp_dev-6.7.2.dev251}/src/ha_mcp/tools/tools_config_info.py +0 -0
- {ha_mcp_dev-6.7.2.dev250 → ha_mcp_dev-6.7.2.dev251}/src/ha_mcp/tools/tools_config_scripts.py +0 -0
- {ha_mcp_dev-6.7.2.dev250 → ha_mcp_dev-6.7.2.dev251}/src/ha_mcp/tools/tools_entities.py +0 -0
- {ha_mcp_dev-6.7.2.dev250 → ha_mcp_dev-6.7.2.dev251}/src/ha_mcp/tools/tools_filesystem.py +0 -0
- {ha_mcp_dev-6.7.2.dev250 → ha_mcp_dev-6.7.2.dev251}/src/ha_mcp/tools/tools_groups.py +0 -0
- {ha_mcp_dev-6.7.2.dev250 → ha_mcp_dev-6.7.2.dev251}/src/ha_mcp/tools/tools_hacs.py +0 -0
- {ha_mcp_dev-6.7.2.dev250 → ha_mcp_dev-6.7.2.dev251}/src/ha_mcp/tools/tools_history.py +0 -0
- {ha_mcp_dev-6.7.2.dev250 → ha_mcp_dev-6.7.2.dev251}/src/ha_mcp/tools/tools_integrations.py +0 -0
- {ha_mcp_dev-6.7.2.dev250 → ha_mcp_dev-6.7.2.dev251}/src/ha_mcp/tools/tools_labels.py +0 -0
- {ha_mcp_dev-6.7.2.dev250 → ha_mcp_dev-6.7.2.dev251}/src/ha_mcp/tools/tools_mcp_component.py +0 -0
- {ha_mcp_dev-6.7.2.dev250 → ha_mcp_dev-6.7.2.dev251}/src/ha_mcp/tools/tools_registry.py +0 -0
- {ha_mcp_dev-6.7.2.dev250 → ha_mcp_dev-6.7.2.dev251}/src/ha_mcp/tools/tools_resources.py +0 -0
- {ha_mcp_dev-6.7.2.dev250 → ha_mcp_dev-6.7.2.dev251}/src/ha_mcp/tools/tools_search.py +0 -0
- {ha_mcp_dev-6.7.2.dev250 → ha_mcp_dev-6.7.2.dev251}/src/ha_mcp/tools/tools_service.py +0 -0
- {ha_mcp_dev-6.7.2.dev250 → ha_mcp_dev-6.7.2.dev251}/src/ha_mcp/tools/tools_services.py +0 -0
- {ha_mcp_dev-6.7.2.dev250 → ha_mcp_dev-6.7.2.dev251}/src/ha_mcp/tools/tools_system.py +0 -0
- {ha_mcp_dev-6.7.2.dev250 → ha_mcp_dev-6.7.2.dev251}/src/ha_mcp/tools/tools_todo.py +0 -0
- {ha_mcp_dev-6.7.2.dev250 → ha_mcp_dev-6.7.2.dev251}/src/ha_mcp/tools/tools_traces.py +0 -0
- {ha_mcp_dev-6.7.2.dev250 → ha_mcp_dev-6.7.2.dev251}/src/ha_mcp/tools/tools_utility.py +0 -0
- {ha_mcp_dev-6.7.2.dev250 → ha_mcp_dev-6.7.2.dev251}/src/ha_mcp/tools/tools_voice_assistant.py +0 -0
- {ha_mcp_dev-6.7.2.dev250 → ha_mcp_dev-6.7.2.dev251}/src/ha_mcp/tools/tools_zones.py +0 -0
- {ha_mcp_dev-6.7.2.dev250 → ha_mcp_dev-6.7.2.dev251}/src/ha_mcp/tools/util_helpers.py +0 -0
- {ha_mcp_dev-6.7.2.dev250 → ha_mcp_dev-6.7.2.dev251}/src/ha_mcp/utils/__init__.py +0 -0
- {ha_mcp_dev-6.7.2.dev250 → ha_mcp_dev-6.7.2.dev251}/src/ha_mcp/utils/domain_handlers.py +0 -0
- {ha_mcp_dev-6.7.2.dev250 → ha_mcp_dev-6.7.2.dev251}/src/ha_mcp/utils/fuzzy_search.py +0 -0
- {ha_mcp_dev-6.7.2.dev250 → ha_mcp_dev-6.7.2.dev251}/src/ha_mcp/utils/operation_manager.py +0 -0
- {ha_mcp_dev-6.7.2.dev250 → ha_mcp_dev-6.7.2.dev251}/src/ha_mcp/utils/python_sandbox.py +0 -0
- {ha_mcp_dev-6.7.2.dev250 → ha_mcp_dev-6.7.2.dev251}/src/ha_mcp/utils/usage_logger.py +0 -0
- {ha_mcp_dev-6.7.2.dev250 → ha_mcp_dev-6.7.2.dev251}/src/ha_mcp_dev.egg-info/SOURCES.txt +0 -0
- {ha_mcp_dev-6.7.2.dev250 → ha_mcp_dev-6.7.2.dev251}/src/ha_mcp_dev.egg-info/dependency_links.txt +0 -0
- {ha_mcp_dev-6.7.2.dev250 → ha_mcp_dev-6.7.2.dev251}/src/ha_mcp_dev.egg-info/entry_points.txt +0 -0
- {ha_mcp_dev-6.7.2.dev250 → ha_mcp_dev-6.7.2.dev251}/src/ha_mcp_dev.egg-info/requires.txt +0 -0
- {ha_mcp_dev-6.7.2.dev250 → ha_mcp_dev-6.7.2.dev251}/src/ha_mcp_dev.egg-info/top_level.txt +0 -0
- {ha_mcp_dev-6.7.2.dev250 → ha_mcp_dev-6.7.2.dev251}/tests/__init__.py +0 -0
- {ha_mcp_dev-6.7.2.dev250 → ha_mcp_dev-6.7.2.dev251}/tests/test_constants.py +0 -0
- {ha_mcp_dev-6.7.2.dev250 → ha_mcp_dev-6.7.2.dev251}/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 = "6.7.2.
|
|
7
|
+
version = "6.7.2.dev251"
|
|
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,6 +5,7 @@ This module provides tools for listing available updates, getting release notes,
|
|
|
5
5
|
and retrieving system version information.
|
|
6
6
|
"""
|
|
7
7
|
|
|
8
|
+
import asyncio
|
|
8
9
|
import logging
|
|
9
10
|
import re
|
|
10
11
|
from typing import Annotated, Any
|
|
@@ -19,6 +20,188 @@ from .util_helpers import coerce_bool_param
|
|
|
19
20
|
|
|
20
21
|
logger = logging.getLogger(__name__)
|
|
21
22
|
|
|
23
|
+
_GITHUB_CORE_RELEASE_URL = (
|
|
24
|
+
"https://api.github.com/repos/home-assistant/core/releases/tags/{version}"
|
|
25
|
+
)
|
|
26
|
+
_MAX_MONTHLY_VERSIONS = 12
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def _parse_version(version_str: str) -> tuple[int, ...] | None:
|
|
30
|
+
"""Parse '2025.11.3' into a comparable tuple, or None on failure."""
|
|
31
|
+
if not version_str:
|
|
32
|
+
return None
|
|
33
|
+
try:
|
|
34
|
+
return tuple(int(x) for x in version_str.split("."))
|
|
35
|
+
except (ValueError, AttributeError):
|
|
36
|
+
return None
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def _get_monthly_versions_between(current: str, target: str) -> list[str]:
|
|
40
|
+
"""Return .0 monthly versions between current (exclusive) and target (inclusive)."""
|
|
41
|
+
current_parts = _parse_version(current)
|
|
42
|
+
target_parts = _parse_version(target)
|
|
43
|
+
if not current_parts or not target_parts or len(current_parts) < 2 or len(target_parts) < 2:
|
|
44
|
+
if target_parts and len(target_parts) >= 2:
|
|
45
|
+
return [f"{target_parts[0]}.{target_parts[1]}.0"]
|
|
46
|
+
return []
|
|
47
|
+
|
|
48
|
+
versions: list[str] = []
|
|
49
|
+
year, month = current_parts[0], current_parts[1] + 1
|
|
50
|
+
while (year, month) <= (target_parts[0], target_parts[1]):
|
|
51
|
+
versions.append(f"{year}.{month}.0")
|
|
52
|
+
month += 1
|
|
53
|
+
if month > 12:
|
|
54
|
+
month, year = 1, year + 1
|
|
55
|
+
if len(versions) >= _MAX_MONTHLY_VERSIONS:
|
|
56
|
+
break
|
|
57
|
+
return versions
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def _strip_html(html: str) -> str:
|
|
61
|
+
"""Strip HTML tags and normalise whitespace for readable plain text."""
|
|
62
|
+
text = re.sub(r"<br\s*/?>", "\n", html)
|
|
63
|
+
text = re.sub(r"<p[^>]*>", "\n", text)
|
|
64
|
+
text = re.sub(r"</p>", "\n", text)
|
|
65
|
+
text = re.sub(r"<li[^>]*>", "\n- ", text)
|
|
66
|
+
text = re.sub(r"<[^>]+>", "", text)
|
|
67
|
+
text = re.sub(r"[ \t]+", " ", text)
|
|
68
|
+
text = re.sub(r"\n{3,}", "\n\n", text)
|
|
69
|
+
return text.strip()
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def _extract_blog_content(html: str) -> str:
|
|
73
|
+
"""Extract article body from an HA blog post as plain text."""
|
|
74
|
+
article = re.search(r"<article[^>]*>(.*?)</article>", html, re.DOTALL)
|
|
75
|
+
if article:
|
|
76
|
+
return _strip_html(article.group(1))
|
|
77
|
+
content = re.search(
|
|
78
|
+
r"(<h[12][^>]*>.*?)(?=<div[^>]*id=\"discourse|<footer[^>]*>|</body>)",
|
|
79
|
+
html, re.DOTALL | re.IGNORECASE,
|
|
80
|
+
)
|
|
81
|
+
if content:
|
|
82
|
+
return _strip_html(content.group(1))
|
|
83
|
+
return _strip_html(html)
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def _parse_breaking_changes_html(html: str, source_url: str) -> dict[str, Any] | None:
|
|
87
|
+
"""Extract 'Backward-incompatible changes' section entries from blog HTML."""
|
|
88
|
+
section_match = re.search(
|
|
89
|
+
r'id="backward-incompatible-changes"[^>]*>.*?</h2>(.*?)(?=<h2[ >]|</article>|</main>)',
|
|
90
|
+
html, re.DOTALL | re.IGNORECASE,
|
|
91
|
+
)
|
|
92
|
+
if not section_match:
|
|
93
|
+
return None
|
|
94
|
+
|
|
95
|
+
entries: list[dict[str, str]] = []
|
|
96
|
+
for match in re.finditer(
|
|
97
|
+
r"<h3[^>]*>(.*?)</h3>(.*?)(?=<h3[^>]*>|$)", section_match.group(1), re.DOTALL
|
|
98
|
+
):
|
|
99
|
+
name = _strip_html(match.group(1)).strip()
|
|
100
|
+
desc = _strip_html(match.group(2)).strip()
|
|
101
|
+
if name:
|
|
102
|
+
entries.append({"integration": name, "description": desc})
|
|
103
|
+
|
|
104
|
+
if not entries:
|
|
105
|
+
return None
|
|
106
|
+
return {"entries": entries, "count": len(entries), "source_url": source_url}
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def _parse_patch_breaking_changes(body: str, version: str) -> dict[str, Any] | None:
|
|
110
|
+
"""Parse (breaking-change) tagged items from a GitHub patch-release body."""
|
|
111
|
+
entries: list[dict[str, str]] = []
|
|
112
|
+
for line in body.split("\n"):
|
|
113
|
+
if "(breaking-change)" not in line.lower():
|
|
114
|
+
continue
|
|
115
|
+
clean = re.sub(r"\(breaking-change\)", "", line, flags=re.IGNORECASE).lstrip("-*").strip()
|
|
116
|
+
if not clean:
|
|
117
|
+
continue
|
|
118
|
+
doc_match = re.search(r"\[([^\]]+?)\s+(?:docs|documentation)\]", clean, re.IGNORECASE)
|
|
119
|
+
integration = doc_match.group(1).strip() if doc_match else "unknown"
|
|
120
|
+
entries.append({"integration": integration, "description": clean})
|
|
121
|
+
|
|
122
|
+
if not entries:
|
|
123
|
+
return None
|
|
124
|
+
return {
|
|
125
|
+
"entries": entries,
|
|
126
|
+
"count": len(entries),
|
|
127
|
+
"source_url": f"https://github.com/home-assistant/core/releases/tag/{version}",
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
async def _fetch_release_data_for_version(
|
|
132
|
+
http_client: httpx.AsyncClient, version: str
|
|
133
|
+
) -> dict[str, Any] | None:
|
|
134
|
+
"""Fetch release notes and breaking changes for a single HA Core version."""
|
|
135
|
+
try:
|
|
136
|
+
resp = await http_client.get(_GITHUB_CORE_RELEASE_URL.format(version=version))
|
|
137
|
+
if resp.status_code != 200:
|
|
138
|
+
return None
|
|
139
|
+
body = resp.json().get("body", "").strip()
|
|
140
|
+
|
|
141
|
+
if body.startswith("https://www.home-assistant.io/blog/"):
|
|
142
|
+
blog_resp = await http_client.get(body)
|
|
143
|
+
if blog_resp.status_code == 200:
|
|
144
|
+
bc = _parse_breaking_changes_html(blog_resp.text, body)
|
|
145
|
+
return {
|
|
146
|
+
"entries": bc["entries"] if bc else [],
|
|
147
|
+
"count": bc["count"] if bc else 0,
|
|
148
|
+
"source_url": body,
|
|
149
|
+
"release_notes": _extract_blog_content(blog_resp.text),
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
if "(breaking-change)" in body.lower():
|
|
153
|
+
return _parse_patch_breaking_changes(body, version)
|
|
154
|
+
return None
|
|
155
|
+
except (httpx.RequestError, ValueError, KeyError) as e:
|
|
156
|
+
logger.debug(f"Failed to fetch release data for {version}: {e}")
|
|
157
|
+
return None
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
async def _fetch_release_data(current_version: str, target_version: str) -> dict[str, Any]:
|
|
161
|
+
"""Fetch release notes and breaking changes for all monthly versions in range."""
|
|
162
|
+
monthly = _get_monthly_versions_between(current_version, target_version)
|
|
163
|
+
if not monthly:
|
|
164
|
+
return {"entries": [], "count": 0, "versions_checked": [], "release_notes": []}
|
|
165
|
+
|
|
166
|
+
async with httpx.AsyncClient(
|
|
167
|
+
timeout=20.0, follow_redirects=True,
|
|
168
|
+
headers={"User-Agent": "HomeAssistant-MCP-Server", "Accept": "application/vnd.github+json"},
|
|
169
|
+
) as http_client:
|
|
170
|
+
results = await asyncio.gather(
|
|
171
|
+
*[_fetch_release_data_for_version(http_client, v) for v in monthly],
|
|
172
|
+
return_exceptions=True,
|
|
173
|
+
)
|
|
174
|
+
|
|
175
|
+
all_entries: list[dict[str, Any]] = []
|
|
176
|
+
versions_checked: list[str] = []
|
|
177
|
+
release_notes: list[dict[str, str]] = []
|
|
178
|
+
|
|
179
|
+
for version, result in zip(monthly, results, strict=True):
|
|
180
|
+
if not isinstance(result, dict):
|
|
181
|
+
continue
|
|
182
|
+
versions_checked.append(version)
|
|
183
|
+
src = result.get("source_url", "")
|
|
184
|
+
all_entries.extend({**entry, "version": version} for entry in result.get("entries", []))
|
|
185
|
+
notes = result.get("release_notes", "")
|
|
186
|
+
if notes:
|
|
187
|
+
release_notes.append({"version": version, "content": notes, "source_url": src})
|
|
188
|
+
|
|
189
|
+
return {
|
|
190
|
+
"entries": all_entries, "count": len(all_entries),
|
|
191
|
+
"versions_checked": versions_checked, "release_notes": release_notes,
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
async def _get_installed_integration_domains(client: Any) -> set[str]:
|
|
196
|
+
"""Get installed integration domains from config entries."""
|
|
197
|
+
try:
|
|
198
|
+
entries = await client._request("GET", "/config/config_entries/entry")
|
|
199
|
+
if isinstance(entries, list):
|
|
200
|
+
return {e.get("domain", "") for e in entries} - {""}
|
|
201
|
+
return set()
|
|
202
|
+
except (httpx.RequestError, ValueError, KeyError):
|
|
203
|
+
return set()
|
|
204
|
+
|
|
22
205
|
|
|
23
206
|
def register_update_tools(mcp: Any, client: Any, **kwargs: Any) -> None:
|
|
24
207
|
"""Register Home Assistant update management tools."""
|
|
@@ -104,7 +287,9 @@ def register_update_tools(mcp: Any, client: Any, **kwargs: Any) -> None:
|
|
|
104
287
|
"include_skipped": include_skipped,
|
|
105
288
|
}
|
|
106
289
|
|
|
107
|
-
async def _get_update_details(
|
|
290
|
+
async def _get_update_details(
|
|
291
|
+
entity_id: str, include_release_notes: bool = False
|
|
292
|
+
) -> dict[str, Any]:
|
|
108
293
|
"""Internal helper to get details for a specific update entity."""
|
|
109
294
|
# Validate entity_id format
|
|
110
295
|
if not entity_id.startswith("update."):
|
|
@@ -187,6 +372,26 @@ def register_update_tools(mcp: Any, client: Any, **kwargs: Any) -> None:
|
|
|
187
372
|
f"View them at: {release_url}"
|
|
188
373
|
)
|
|
189
374
|
|
|
375
|
+
# Include multi-version breaking change analysis for Core updates
|
|
376
|
+
if include_release_notes and result.get("category") == "core":
|
|
377
|
+
installed = result.get("installed_version")
|
|
378
|
+
target = result.get("latest_version")
|
|
379
|
+
if installed and target:
|
|
380
|
+
rd_result, domains_result = await asyncio.gather(
|
|
381
|
+
_fetch_release_data(installed, target),
|
|
382
|
+
_get_installed_integration_domains(client),
|
|
383
|
+
return_exceptions=True,
|
|
384
|
+
)
|
|
385
|
+
rd = rd_result if isinstance(rd_result, dict) else {}
|
|
386
|
+
domains = domains_result if isinstance(domains_result, set) else set()
|
|
387
|
+
result["installed_integrations"] = sorted(domains)
|
|
388
|
+
result["multi_version_release_notes"] = rd.get("release_notes", [])
|
|
389
|
+
result["breaking_changes"] = {
|
|
390
|
+
"entries": rd.get("entries", []),
|
|
391
|
+
"count": rd.get("count", 0),
|
|
392
|
+
"versions_checked": rd.get("versions_checked", []),
|
|
393
|
+
}
|
|
394
|
+
|
|
190
395
|
return result
|
|
191
396
|
|
|
192
397
|
@mcp.tool(annotations={"idempotentHint": True, "openWorldHint": True, "readOnlyHint": True, "tags": ["update"], "title": "Get Updates"})
|
|
@@ -207,6 +412,15 @@ def register_update_tools(mcp: Any, client: Any, **kwargs: Any) -> None:
|
|
|
207
412
|
default=False,
|
|
208
413
|
),
|
|
209
414
|
] = False,
|
|
415
|
+
include_release_notes: Annotated[
|
|
416
|
+
bool | str,
|
|
417
|
+
Field(
|
|
418
|
+
description="When getting a Core update entity, fetch multi-version release notes "
|
|
419
|
+
"and breaking changes for all versions between installed and latest (default: False). "
|
|
420
|
+
"Adds breaking_changes, multi_version_release_notes, and installed_integrations to the response.",
|
|
421
|
+
default=False,
|
|
422
|
+
),
|
|
423
|
+
] = False,
|
|
210
424
|
) -> dict[str, Any]:
|
|
211
425
|
"""
|
|
212
426
|
Get update information - list all updates or get details for a specific one.
|
|
@@ -217,10 +431,15 @@ def register_update_tools(mcp: Any, client: Any, **kwargs: Any) -> None:
|
|
|
217
431
|
With an entity_id: Returns detailed information about a specific update including
|
|
218
432
|
version info, category, and release notes (if available).
|
|
219
433
|
|
|
434
|
+
With include_release_notes=True (Core updates only): Also fetches HA release
|
|
435
|
+
blog posts for every monthly version between installed and latest. Returns
|
|
436
|
+
structured breaking changes and installed integration domains for cross-referencing.
|
|
437
|
+
|
|
220
438
|
EXAMPLES:
|
|
221
439
|
- List all updates: ha_get_updates()
|
|
222
440
|
- List including skipped: ha_get_updates(include_skipped=True)
|
|
223
441
|
- Get specific update: ha_get_updates(entity_id="update.home_assistant_core_update")
|
|
442
|
+
- Pre-update analysis: ha_get_updates(entity_id="update.home_assistant_core_update", include_release_notes=True)
|
|
224
443
|
|
|
225
444
|
RETURNS (when listing):
|
|
226
445
|
- updates_available: Count of available updates
|
|
@@ -231,6 +450,11 @@ def register_update_tools(mcp: Any, client: Any, **kwargs: Any) -> None:
|
|
|
231
450
|
- Update details including installed/latest versions
|
|
232
451
|
- Release notes (fetched from WebSocket API or GitHub)
|
|
233
452
|
- Category and installation status
|
|
453
|
+
|
|
454
|
+
RETURNS (with include_release_notes=True, Core only):
|
|
455
|
+
- breaking_changes.entries[]: Each has integration, description, version
|
|
456
|
+
- multi_version_release_notes[]: Full text per version {version, content, source_url}
|
|
457
|
+
- installed_integrations: Your integration domains for cross-referencing
|
|
234
458
|
"""
|
|
235
459
|
try:
|
|
236
460
|
if entity_id is None:
|
|
@@ -241,7 +465,10 @@ def register_update_tools(mcp: Any, client: Any, **kwargs: Any) -> None:
|
|
|
241
465
|
return await _list_updates(include_skipped_bool)
|
|
242
466
|
else:
|
|
243
467
|
# Get mode: return details for specific update
|
|
244
|
-
|
|
468
|
+
include_rn_bool = coerce_bool_param(
|
|
469
|
+
include_release_notes, "include_release_notes", default=False
|
|
470
|
+
) or False
|
|
471
|
+
return await _get_update_details(entity_id, include_rn_bool)
|
|
245
472
|
|
|
246
473
|
except ToolError:
|
|
247
474
|
raise
|
|
@@ -260,6 +487,7 @@ def register_update_tools(mcp: Any, client: Any, **kwargs: Any) -> None:
|
|
|
260
487
|
"Verify API access permissions",
|
|
261
488
|
])
|
|
262
489
|
|
|
490
|
+
|
|
263
491
|
def _supports_release_notes(entity_id: str, attributes: dict[str, Any]) -> bool:
|
|
264
492
|
"""
|
|
265
493
|
Determine if an update entity supports fetching release notes.
|
|
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
|
|
File without changes
|
{ha_mcp_dev-6.7.2.dev250 → ha_mcp_dev-6.7.2.dev251}/src/ha_mcp/resources/skills-vendor/AGENTS.md
RENAMED
|
File without changes
|
{ha_mcp_dev-6.7.2.dev250 → ha_mcp_dev-6.7.2.dev251}/src/ha_mcp/resources/skills-vendor/CLAUDE.md
RENAMED
|
File without changes
|
|
File without changes
|
{ha_mcp_dev-6.7.2.dev250 → ha_mcp_dev-6.7.2.dev251}/src/ha_mcp/resources/skills-vendor/LICENSE
RENAMED
|
File without changes
|
{ha_mcp_dev-6.7.2.dev250 → ha_mcp_dev-6.7.2.dev251}/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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{ha_mcp_dev-6.7.2.dev250 → ha_mcp_dev-6.7.2.dev251}/src/ha_mcp/tools/tools_config_automations.py
RENAMED
|
File without changes
|
{ha_mcp_dev-6.7.2.dev250 → ha_mcp_dev-6.7.2.dev251}/src/ha_mcp/tools/tools_config_dashboards.py
RENAMED
|
File without changes
|
{ha_mcp_dev-6.7.2.dev250 → ha_mcp_dev-6.7.2.dev251}/src/ha_mcp/tools/tools_config_entry_flow.py
RENAMED
|
File without changes
|
{ha_mcp_dev-6.7.2.dev250 → ha_mcp_dev-6.7.2.dev251}/src/ha_mcp/tools/tools_config_helpers.py
RENAMED
|
File without changes
|
|
File without changes
|
{ha_mcp_dev-6.7.2.dev250 → ha_mcp_dev-6.7.2.dev251}/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-6.7.2.dev250 → ha_mcp_dev-6.7.2.dev251}/src/ha_mcp/tools/tools_voice_assistant.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
|
{ha_mcp_dev-6.7.2.dev250 → ha_mcp_dev-6.7.2.dev251}/src/ha_mcp_dev.egg-info/dependency_links.txt
RENAMED
|
File without changes
|
{ha_mcp_dev-6.7.2.dev250 → ha_mcp_dev-6.7.2.dev251}/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
|