Skip to content

Commit e09ac61

Browse files
julienldclaude
andcommitted
Merge branch 'master' into feat/pyinstaller-binary
Resolve conflicts: - Keep our PRIVACY.md and README.md privacy section - Keep our build-binary.yml workflow with mcpb support - Keep our manifest.template.json and generate_manifest.py - Merge util_helpers.py to include coerce_bool_param from master - Merge tools_utility.py with coerce_bool_param and keep title annotation - Remove duplicate icon-light-* files (main icons are now the light versions) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
2 parents 6d800ef + 7931b3f commit e09ac61

14 files changed

Lines changed: 324 additions & 36 deletions

CHANGELOG.md

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,56 @@
22

33
<!-- version list -->
44

5+
## v4.7.7 (2025-12-01)
6+
7+
### Bug Fixes
8+
9+
- Normalize automation GET config for round-trip compatibility (#221)
10+
([#221](https://github.com/homeassistant-ai/ha-mcp/pull/221),
11+
[`278b7a5`](https://github.com/homeassistant-ai/ha-mcp/commit/278b7a5310275389993333b9f63451f86c7385e5))
12+
13+
### Chores
14+
15+
- **addon**: Sync changelog for Home Assistant add-on [skip ci]
16+
([`f9c512b`](https://github.com/homeassistant-ai/ha-mcp/commit/f9c512bcb63fc9fb9ed98c6e22795c90ff7bf323))
17+
18+
19+
## v4.7.6 (2025-12-01)
20+
21+
### Bug Fixes
22+
23+
- Add boolean coercion for string parameters from XML-style calls (#219)
24+
([#219](https://github.com/homeassistant-ai/ha-mcp/pull/219),
25+
[`aa34589`](https://github.com/homeassistant-ai/ha-mcp/commit/aa3458978a8a8cb66f3af6d76d0de4450f5f282a))
26+
27+
### Chores
28+
29+
- **addon**: Sync changelog for Home Assistant add-on [skip ci]
30+
([`f983929`](https://github.com/homeassistant-ai/ha-mcp/commit/f983929b722cbb655364aa9204a4f0eb727a1378))
31+
32+
33+
## v4.7.5 (2025-12-01)
34+
35+
### Bug Fixes
36+
37+
- Add string coercion for numeric parameters (fixes #205, #206) (#217)
38+
([#217](https://github.com/homeassistant-ai/ha-mcp/pull/217),
39+
[`59fc978`](https://github.com/homeassistant-ai/ha-mcp/commit/59fc978afa817c8c5759427608d121cc01c66822))
40+
41+
- Normalize automation config field names (trigger/triggers) (#215)
42+
([#215](https://github.com/homeassistant-ai/ha-mcp/pull/215),
43+
[`c570bdf`](https://github.com/homeassistant-ai/ha-mcp/commit/c570bdf28f80f0240c5d1910560c01922cf49bfd))
44+
45+
- Query area/entity registries for accurate area count in overview (#216)
46+
([#216](https://github.com/homeassistant-ai/ha-mcp/pull/216),
47+
[`3808907`](https://github.com/homeassistant-ai/ha-mcp/commit/3808907c4e6d615f5a770da6f09c36077e377bc9))
48+
49+
### Chores
50+
51+
- **addon**: Sync changelog for Home Assistant add-on [skip ci]
52+
([`645ca39`](https://github.com/homeassistant-ai/ha-mcp/commit/645ca39f300a9799ad9f5ecc35780bde483f20e2))
53+
54+
555
## v4.7.4 (2025-11-29)
656

757
### Bug Fixes

homeassistant-addon/CHANGELOG.md

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,56 @@
22

33
<!-- version list -->
44

5+
## v4.7.7 (2025-12-01)
6+
7+
### Bug Fixes
8+
9+
- Normalize automation GET config for round-trip compatibility (#221)
10+
([#221](https://github.com/homeassistant-ai/ha-mcp/pull/221),
11+
[`278b7a5`](https://github.com/homeassistant-ai/ha-mcp/commit/278b7a5310275389993333b9f63451f86c7385e5))
12+
13+
### Chores
14+
15+
- **addon**: Sync changelog for Home Assistant add-on [skip ci]
16+
([`f9c512b`](https://github.com/homeassistant-ai/ha-mcp/commit/f9c512bcb63fc9fb9ed98c6e22795c90ff7bf323))
17+
18+
19+
## v4.7.6 (2025-12-01)
20+
21+
### Bug Fixes
22+
23+
- Add boolean coercion for string parameters from XML-style calls (#219)
24+
([#219](https://github.com/homeassistant-ai/ha-mcp/pull/219),
25+
[`aa34589`](https://github.com/homeassistant-ai/ha-mcp/commit/aa3458978a8a8cb66f3af6d76d0de4450f5f282a))
26+
27+
### Chores
28+
29+
- **addon**: Sync changelog for Home Assistant add-on [skip ci]
30+
([`f983929`](https://github.com/homeassistant-ai/ha-mcp/commit/f983929b722cbb655364aa9204a4f0eb727a1378))
31+
32+
33+
## v4.7.5 (2025-12-01)
34+
35+
### Bug Fixes
36+
37+
- Add string coercion for numeric parameters (fixes #205, #206) (#217)
38+
([#217](https://github.com/homeassistant-ai/ha-mcp/pull/217),
39+
[`59fc978`](https://github.com/homeassistant-ai/ha-mcp/commit/59fc978afa817c8c5759427608d121cc01c66822))
40+
41+
- Normalize automation config field names (trigger/triggers) (#215)
42+
([#215](https://github.com/homeassistant-ai/ha-mcp/pull/215),
43+
[`c570bdf`](https://github.com/homeassistant-ai/ha-mcp/commit/c570bdf28f80f0240c5d1910560c01922cf49bfd))
44+
45+
- Query area/entity registries for accurate area count in overview (#216)
46+
([#216](https://github.com/homeassistant-ai/ha-mcp/pull/216),
47+
[`3808907`](https://github.com/homeassistant-ai/ha-mcp/commit/3808907c4e6d615f5a770da6f09c36077e377bc9))
48+
49+
### Chores
50+
51+
- **addon**: Sync changelog for Home Assistant add-on [skip ci]
52+
([`645ca39`](https://github.com/homeassistant-ai/ha-mcp/commit/645ca39f300a9799ad9f5ecc35780bde483f20e2))
53+
54+
555
## v4.7.4 (2025-11-29)
656

757
### Bug Fixes

homeassistant-addon/config.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
name: "Home Assistant MCP Server"
22
description: "AI assistant integration for Home Assistant via Model Context Protocol (MCP)"
3-
version: "4.7.4"
3+
version: "4.7.7"
44
slug: "ha_mcp"
55
url: "https://github.com/homeassistant-ai/ha-mcp"
66
arch:

packaging/mcpb/icon.png

4.01 KB
Loading

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
44

55
[project]
66
name = "ha-mcp"
7-
version = "4.7.4"
7+
version = "4.7.7"
88
description = "Home Assistant MCP Server - Complete control of Home Assistant through MCP"
99
readme = "README.md"
1010
requires-python = ">=3.13,<3.14"

src/ha_mcp/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
through REST API and WebSocket integration with 20+ enhanced tools.
66
"""
77

8-
__version__ = "4.7.4"
8+
__version__ = "4.7.7"
99
__author__ = "Julien"
1010
__license__ = "MIT"
1111

src/ha_mcp/tools/smart_search.py

Lines changed: 33 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -240,6 +240,36 @@ async def get_system_overview(
240240
entities = await self.client.get_states()
241241
services = await self.client.get_services()
242242

243+
# Get area registry and entity registry for proper area mapping
244+
# Areas exist in the registry, not in entity state attributes
245+
area_registry: list[dict[str, Any]] = []
246+
entity_registry: list[dict[str, Any]] = []
247+
try:
248+
area_result = await self.client.send_websocket_message(
249+
{"type": "config/area_registry/list"}
250+
)
251+
if area_result.get("success"):
252+
area_registry = area_result.get("result", [])
253+
except Exception as e:
254+
logger.debug(f"Could not fetch area registry: {e}")
255+
256+
try:
257+
entity_result = await self.client.send_websocket_message(
258+
{"type": "config/entity_registry/list"}
259+
)
260+
if entity_result.get("success"):
261+
entity_registry = entity_result.get("result", [])
262+
except Exception as e:
263+
logger.debug(f"Could not fetch entity registry: {e}")
264+
265+
# Build entity_id -> area_id mapping from entity registry
266+
entity_area_map: dict[str, str | None] = {}
267+
for entry in entity_registry:
268+
entity_id = entry.get("entity_id")
269+
area_id = entry.get("area_id")
270+
if entity_id:
271+
entity_area_map[entity_id] = area_id
272+
243273
# Determine defaults based on detail_level
244274
if max_entities_per_domain is None:
245275
max_entities_per_domain = 10 if detail_level == "minimal" else None
@@ -285,8 +315,8 @@ async def get_system_overview(
285315

286316
domain_stats[domain]["all_entities"].append(entity_data)
287317

288-
# Area analysis
289-
area_id = attributes.get("area_id")
318+
# Area analysis - use entity registry mapping, not state attributes
319+
area_id = entity_area_map.get(entity_id)
290320
if area_id:
291321
if area_id not in area_stats:
292322
area_stats[area_id] = {"count": 0, "domains": {}}
@@ -374,7 +404,7 @@ async def get_system_overview(
374404
"total_entities": len(entities),
375405
"total_domains": len(domain_stats),
376406
"total_services": total_services,
377-
"total_areas": len(area_stats),
407+
"total_areas": len(area_registry),
378408
},
379409
"domain_stats": formatted_domain_stats,
380410
"area_analysis": area_stats, # Now included in all detail levels

src/ha_mcp/tools/tools_config_automations.py

Lines changed: 90 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,90 @@
1616
logger = logging.getLogger(__name__)
1717

1818

19+
def _normalize_automation_config(config: dict[str, Any]) -> dict[str, Any]:
20+
"""
21+
Normalize automation config field names to HA API format.
22+
23+
Home Assistant accepts both singular ('trigger', 'action', 'condition')
24+
and plural ('triggers', 'actions', 'conditions') field names in YAML,
25+
but the API expects singular forms. This function normalizes plural
26+
to singular for consistency.
27+
28+
Args:
29+
config: Automation configuration dict
30+
31+
Returns:
32+
Normalized configuration with singular field names
33+
"""
34+
normalized = config.copy()
35+
36+
# Map plural field names to singular (HA API format)
37+
field_mappings = {
38+
"triggers": "trigger",
39+
"actions": "action",
40+
"conditions": "condition",
41+
}
42+
43+
for plural, singular in field_mappings.items():
44+
if plural in normalized and singular not in normalized:
45+
normalized[singular] = normalized.pop(plural)
46+
elif plural in normalized and singular in normalized:
47+
# Both exist - prefer singular, remove plural
48+
del normalized[plural]
49+
50+
return normalized
51+
52+
53+
def _normalize_trigger_keys(triggers: list[dict[str, Any]]) -> list[dict[str, Any]]:
54+
"""
55+
Normalize trigger objects for round-trip compatibility.
56+
57+
Home Assistant GET API returns triggers with 'trigger' key for the platform type,
58+
but the SET API expects 'platform' key. This function converts between formats.
59+
60+
Args:
61+
triggers: List of trigger configuration dicts
62+
63+
Returns:
64+
List of triggers with 'platform' key instead of 'trigger' key
65+
"""
66+
normalized_triggers = []
67+
for trigger in triggers:
68+
normalized_trigger = trigger.copy()
69+
# Convert 'trigger' key to 'platform' if present and 'platform' is not
70+
if "trigger" in normalized_trigger and "platform" not in normalized_trigger:
71+
normalized_trigger["platform"] = normalized_trigger.pop("trigger")
72+
normalized_triggers.append(normalized_trigger)
73+
return normalized_triggers
74+
75+
76+
def _normalize_config_for_roundtrip(config: dict[str, Any]) -> dict[str, Any]:
77+
"""
78+
Normalize automation config from GET response for direct use in SET.
79+
80+
This ensures a config retrieved via ha_config_get_automation can be
81+
directly passed to ha_config_set_automation without modification.
82+
83+
Transformations:
84+
1. Field names: triggers -> trigger, actions -> action, conditions -> condition
85+
2. Trigger keys: trigger -> platform (inside each trigger object)
86+
87+
Args:
88+
config: Raw automation configuration from HA API
89+
90+
Returns:
91+
Normalized configuration compatible with SET API
92+
"""
93+
# First normalize field names (plural -> singular)
94+
normalized = _normalize_automation_config(config)
95+
96+
# Then normalize trigger keys (trigger -> platform)
97+
if "trigger" in normalized and isinstance(normalized["trigger"], list):
98+
normalized["trigger"] = _normalize_trigger_keys(normalized["trigger"])
99+
100+
return normalized
101+
102+
19103
def register_config_automation_tools(mcp: Any, client: Any, **kwargs: Any) -> None:
20104
"""Register Home Assistant automation configuration tools."""
21105

@@ -42,11 +126,13 @@ async def ha_config_get_automation(
42126
"""
43127
try:
44128
config_result = await client.get_automation_config(identifier)
129+
# Normalize config for round-trip compatibility (GET → SET)
130+
normalized_config = _normalize_config_for_roundtrip(config_result)
45131
return {
46132
"success": True,
47133
"action": "get",
48134
"identifier": identifier,
49-
"config": config_result,
135+
"config": normalized_config,
50136
}
51137
except Exception as e:
52138
# Handle 404 errors gracefully (often used to verify deletion)
@@ -186,6 +272,9 @@ async def ha_config_set_automation(
186272

187273
config_dict = cast(dict[str, Any], parsed_config)
188274

275+
# Normalize field names (triggers -> trigger, actions -> action, etc.)
276+
config_dict = _normalize_automation_config(config_dict)
277+
189278
# Validate required fields
190279
required_fields = ["alias", "trigger", "action"]
191280
missing_fields = [f for f in required_fields if f not in config_dict]

src/ha_mcp/tools/tools_search.py

Lines changed: 15 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
from pydantic import Field
1010

1111
from .helpers import log_tool_usage
12-
from .util_helpers import add_timezone_metadata, parse_string_list_param
12+
from .util_helpers import add_timezone_metadata, coerce_bool_param, parse_string_list_param
1313

1414

1515
def register_search_tools(mcp, client, **kwargs):
@@ -25,7 +25,7 @@ async def ha_search_entities(
2525
domain_filter: str | None = None,
2626
area_filter: str | None = None,
2727
limit: int = 10,
28-
group_by_domain: bool = False,
28+
group_by_domain: bool | str = False,
2929
) -> dict[str, Any]:
3030
"""Comprehensive entity search with fuzzy matching, domain/area filtering, and optional grouping.
3131
@@ -45,6 +45,9 @@ async def ha_search_entities(
4545
- 'minimal': Quick orientation (10 entities per domain sample) - RECOMMENDED for searches
4646
- 'standard': Complete picture (all entities, friendly names only) - for comprehensive tasks
4747
- 'full': Maximum detail (includes states, device types, services) - for deep analysis"""
48+
# Coerce boolean parameter that may come as string from XML-style calls
49+
group_by_domain_bool = coerce_bool_param(group_by_domain, "group_by_domain", default=False) or False
50+
4851
try:
4952
# If area_filter is provided, use area-based search
5053
if area_filter:
@@ -105,7 +108,7 @@ async def ha_search_entities(
105108
)
106109

107110
# Group by domain if requested
108-
if group_by_domain:
111+
if group_by_domain_bool:
109112
by_domain: dict[str, list[dict[str, Any]]] = {}
110113
for result in results:
111114
domain = result["domain"]
@@ -204,7 +207,7 @@ async def ha_search_entities(
204207
"search_type": "domain_listing",
205208
"note": f"Listing all {domain_filter} entities (empty query with domain_filter)",
206209
}
207-
if group_by_domain:
210+
if group_by_domain_bool:
208211
domain_list_data["by_domain"] = {domain_filter: results}
209212
return await add_timezone_metadata(client, domain_list_data)
210213

@@ -224,7 +227,7 @@ async def ha_search_entities(
224227
result["domain_filter"] = domain_filter
225228

226229
# Group by domain if requested
227-
if group_by_domain and "results" in result:
230+
if group_by_domain_bool and "results" in result:
228231
by_domain = {}
229232
for entity in result["results"]:
230233
domain = entity.get("domain", entity["entity_id"].split(".")[0])
@@ -273,14 +276,14 @@ async def ha_get_overview(
273276
),
274277
] = None,
275278
include_state: Annotated[
276-
bool | None,
279+
bool | str | None,
277280
Field(
278281
default=None,
279282
description="Include state field for entities (None = auto based on level). Full defaults to True.",
280283
),
281284
] = None,
282285
include_entity_id: Annotated[
283-
bool | None,
286+
bool | str | None,
284287
Field(
285288
default=None,
286289
description="Include entity_id field for entities (None = auto based on level). Full defaults to True.",
@@ -292,8 +295,12 @@ async def ha_get_overview(
292295
Returns comprehensive system information at the requested detail level.
293296
Use 'standard' (default) for most queries. Optionally customize entity fields and limits.
294297
"""
298+
# Coerce boolean parameters that may come as strings from XML-style calls
299+
include_state_bool = coerce_bool_param(include_state, "include_state", default=None)
300+
include_entity_id_bool = coerce_bool_param(include_entity_id, "include_entity_id", default=None)
301+
295302
result = await smart_tools.get_system_overview(
296-
detail_level, max_entities_per_domain, include_state, include_entity_id
303+
detail_level, max_entities_per_domain, include_state_bool, include_entity_id_bool
297304
)
298305
return cast(dict[str, Any], result)
299306

0 commit comments

Comments
 (0)