ha-mcp-dev 7.1.0.dev297__tar.gz → 7.1.0.dev299__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.1.0.dev297/src/ha_mcp_dev.egg-info → ha_mcp_dev-7.1.0.dev299}/PKG-INFO +1 -1
- {ha_mcp_dev-7.1.0.dev297 → ha_mcp_dev-7.1.0.dev299}/pyproject.toml +1 -1
- {ha_mcp_dev-7.1.0.dev297 → ha_mcp_dev-7.1.0.dev299}/src/ha_mcp/tools/smart_search.py +246 -51
- {ha_mcp_dev-7.1.0.dev297 → ha_mcp_dev-7.1.0.dev299}/src/ha_mcp/tools/tools_config_dashboards.py +341 -199
- {ha_mcp_dev-7.1.0.dev297 → ha_mcp_dev-7.1.0.dev299}/src/ha_mcp/tools/tools_integrations.py +90 -47
- {ha_mcp_dev-7.1.0.dev297 → ha_mcp_dev-7.1.0.dev299}/src/ha_mcp/tools/tools_search.py +152 -87
- {ha_mcp_dev-7.1.0.dev297 → ha_mcp_dev-7.1.0.dev299/src/ha_mcp_dev.egg-info}/PKG-INFO +1 -1
- {ha_mcp_dev-7.1.0.dev297 → ha_mcp_dev-7.1.0.dev299}/LICENSE +0 -0
- {ha_mcp_dev-7.1.0.dev297 → ha_mcp_dev-7.1.0.dev299}/MANIFEST.in +0 -0
- {ha_mcp_dev-7.1.0.dev297 → ha_mcp_dev-7.1.0.dev299}/README.md +0 -0
- {ha_mcp_dev-7.1.0.dev297 → ha_mcp_dev-7.1.0.dev299}/setup.cfg +0 -0
- {ha_mcp_dev-7.1.0.dev297 → ha_mcp_dev-7.1.0.dev299}/src/ha_mcp/__init__.py +0 -0
- {ha_mcp_dev-7.1.0.dev297 → ha_mcp_dev-7.1.0.dev299}/src/ha_mcp/__main__.py +0 -0
- {ha_mcp_dev-7.1.0.dev297 → ha_mcp_dev-7.1.0.dev299}/src/ha_mcp/_pypi_marker +0 -0
- {ha_mcp_dev-7.1.0.dev297 → ha_mcp_dev-7.1.0.dev299}/src/ha_mcp/auth/__init__.py +0 -0
- {ha_mcp_dev-7.1.0.dev297 → ha_mcp_dev-7.1.0.dev299}/src/ha_mcp/auth/consent_form.py +0 -0
- {ha_mcp_dev-7.1.0.dev297 → ha_mcp_dev-7.1.0.dev299}/src/ha_mcp/auth/provider.py +0 -0
- {ha_mcp_dev-7.1.0.dev297 → ha_mcp_dev-7.1.0.dev299}/src/ha_mcp/client/__init__.py +0 -0
- {ha_mcp_dev-7.1.0.dev297 → ha_mcp_dev-7.1.0.dev299}/src/ha_mcp/client/rest_client.py +0 -0
- {ha_mcp_dev-7.1.0.dev297 → ha_mcp_dev-7.1.0.dev299}/src/ha_mcp/client/websocket_client.py +0 -0
- {ha_mcp_dev-7.1.0.dev297 → ha_mcp_dev-7.1.0.dev299}/src/ha_mcp/client/websocket_listener.py +0 -0
- {ha_mcp_dev-7.1.0.dev297 → ha_mcp_dev-7.1.0.dev299}/src/ha_mcp/config.py +0 -0
- {ha_mcp_dev-7.1.0.dev297 → ha_mcp_dev-7.1.0.dev299}/src/ha_mcp/errors.py +0 -0
- {ha_mcp_dev-7.1.0.dev297 → ha_mcp_dev-7.1.0.dev299}/src/ha_mcp/py.typed +0 -0
- {ha_mcp_dev-7.1.0.dev297 → ha_mcp_dev-7.1.0.dev299}/src/ha_mcp/resources/card_types.json +0 -0
- {ha_mcp_dev-7.1.0.dev297 → ha_mcp_dev-7.1.0.dev299}/src/ha_mcp/resources/dashboard_guide.md +0 -0
- {ha_mcp_dev-7.1.0.dev297 → ha_mcp_dev-7.1.0.dev299}/src/ha_mcp/resources/skills-vendor/.claude/settings.json +0 -0
- {ha_mcp_dev-7.1.0.dev297 → ha_mcp_dev-7.1.0.dev299}/src/ha_mcp/resources/skills-vendor/.claude-plugin/marketplace.json +0 -0
- {ha_mcp_dev-7.1.0.dev297 → ha_mcp_dev-7.1.0.dev299}/src/ha_mcp/resources/skills-vendor/.claude-plugin/plugin.json +0 -0
- {ha_mcp_dev-7.1.0.dev297 → ha_mcp_dev-7.1.0.dev299}/src/ha_mcp/resources/skills-vendor/.github/ISSUE_TEMPLATE/skill-rca.md +0 -0
- {ha_mcp_dev-7.1.0.dev297 → ha_mcp_dev-7.1.0.dev299}/src/ha_mcp/resources/skills-vendor/AGENTS.md +0 -0
- {ha_mcp_dev-7.1.0.dev297 → ha_mcp_dev-7.1.0.dev299}/src/ha_mcp/resources/skills-vendor/CLAUDE.md +0 -0
- {ha_mcp_dev-7.1.0.dev297 → ha_mcp_dev-7.1.0.dev299}/src/ha_mcp/resources/skills-vendor/CONTRIBUTING.md +0 -0
- {ha_mcp_dev-7.1.0.dev297 → ha_mcp_dev-7.1.0.dev299}/src/ha_mcp/resources/skills-vendor/LICENSE +0 -0
- {ha_mcp_dev-7.1.0.dev297 → ha_mcp_dev-7.1.0.dev299}/src/ha_mcp/resources/skills-vendor/README.md +0 -0
- {ha_mcp_dev-7.1.0.dev297 → ha_mcp_dev-7.1.0.dev299}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/SKILL.md +0 -0
- {ha_mcp_dev-7.1.0.dev297 → ha_mcp_dev-7.1.0.dev299}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/automation-patterns.md +0 -0
- {ha_mcp_dev-7.1.0.dev297 → ha_mcp_dev-7.1.0.dev299}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/device-control.md +0 -0
- {ha_mcp_dev-7.1.0.dev297 → ha_mcp_dev-7.1.0.dev299}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/examples.yaml +0 -0
- {ha_mcp_dev-7.1.0.dev297 → ha_mcp_dev-7.1.0.dev299}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/helper-selection.md +0 -0
- {ha_mcp_dev-7.1.0.dev297 → ha_mcp_dev-7.1.0.dev299}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/safe-refactoring.md +0 -0
- {ha_mcp_dev-7.1.0.dev297 → ha_mcp_dev-7.1.0.dev299}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/template-guidelines.md +0 -0
- {ha_mcp_dev-7.1.0.dev297 → ha_mcp_dev-7.1.0.dev299}/src/ha_mcp/server.py +0 -0
- {ha_mcp_dev-7.1.0.dev297 → ha_mcp_dev-7.1.0.dev299}/src/ha_mcp/smoke_test.py +0 -0
- {ha_mcp_dev-7.1.0.dev297 → ha_mcp_dev-7.1.0.dev299}/src/ha_mcp/tools/__init__.py +0 -0
- {ha_mcp_dev-7.1.0.dev297 → ha_mcp_dev-7.1.0.dev299}/src/ha_mcp/tools/backup.py +0 -0
- {ha_mcp_dev-7.1.0.dev297 → ha_mcp_dev-7.1.0.dev299}/src/ha_mcp/tools/best_practice_checker.py +0 -0
- {ha_mcp_dev-7.1.0.dev297 → ha_mcp_dev-7.1.0.dev299}/src/ha_mcp/tools/device_control.py +0 -0
- {ha_mcp_dev-7.1.0.dev297 → ha_mcp_dev-7.1.0.dev299}/src/ha_mcp/tools/enhanced.py +0 -0
- {ha_mcp_dev-7.1.0.dev297 → ha_mcp_dev-7.1.0.dev299}/src/ha_mcp/tools/helpers.py +0 -0
- {ha_mcp_dev-7.1.0.dev297 → ha_mcp_dev-7.1.0.dev299}/src/ha_mcp/tools/registry.py +0 -0
- {ha_mcp_dev-7.1.0.dev297 → ha_mcp_dev-7.1.0.dev299}/src/ha_mcp/tools/tools_addons.py +0 -0
- {ha_mcp_dev-7.1.0.dev297 → ha_mcp_dev-7.1.0.dev299}/src/ha_mcp/tools/tools_areas.py +0 -0
- {ha_mcp_dev-7.1.0.dev297 → ha_mcp_dev-7.1.0.dev299}/src/ha_mcp/tools/tools_blueprints.py +0 -0
- {ha_mcp_dev-7.1.0.dev297 → ha_mcp_dev-7.1.0.dev299}/src/ha_mcp/tools/tools_bug_report.py +0 -0
- {ha_mcp_dev-7.1.0.dev297 → ha_mcp_dev-7.1.0.dev299}/src/ha_mcp/tools/tools_calendar.py +0 -0
- {ha_mcp_dev-7.1.0.dev297 → ha_mcp_dev-7.1.0.dev299}/src/ha_mcp/tools/tools_camera.py +0 -0
- {ha_mcp_dev-7.1.0.dev297 → ha_mcp_dev-7.1.0.dev299}/src/ha_mcp/tools/tools_config_automations.py +0 -0
- {ha_mcp_dev-7.1.0.dev297 → ha_mcp_dev-7.1.0.dev299}/src/ha_mcp/tools/tools_config_entry_flow.py +0 -0
- {ha_mcp_dev-7.1.0.dev297 → ha_mcp_dev-7.1.0.dev299}/src/ha_mcp/tools/tools_config_helpers.py +0 -0
- {ha_mcp_dev-7.1.0.dev297 → ha_mcp_dev-7.1.0.dev299}/src/ha_mcp/tools/tools_config_info.py +0 -0
- {ha_mcp_dev-7.1.0.dev297 → ha_mcp_dev-7.1.0.dev299}/src/ha_mcp/tools/tools_config_scripts.py +0 -0
- {ha_mcp_dev-7.1.0.dev297 → ha_mcp_dev-7.1.0.dev299}/src/ha_mcp/tools/tools_entities.py +0 -0
- {ha_mcp_dev-7.1.0.dev297 → ha_mcp_dev-7.1.0.dev299}/src/ha_mcp/tools/tools_filesystem.py +0 -0
- {ha_mcp_dev-7.1.0.dev297 → ha_mcp_dev-7.1.0.dev299}/src/ha_mcp/tools/tools_groups.py +0 -0
- {ha_mcp_dev-7.1.0.dev297 → ha_mcp_dev-7.1.0.dev299}/src/ha_mcp/tools/tools_hacs.py +0 -0
- {ha_mcp_dev-7.1.0.dev297 → ha_mcp_dev-7.1.0.dev299}/src/ha_mcp/tools/tools_history.py +0 -0
- {ha_mcp_dev-7.1.0.dev297 → ha_mcp_dev-7.1.0.dev299}/src/ha_mcp/tools/tools_labels.py +0 -0
- {ha_mcp_dev-7.1.0.dev297 → ha_mcp_dev-7.1.0.dev299}/src/ha_mcp/tools/tools_mcp_component.py +0 -0
- {ha_mcp_dev-7.1.0.dev297 → ha_mcp_dev-7.1.0.dev299}/src/ha_mcp/tools/tools_registry.py +0 -0
- {ha_mcp_dev-7.1.0.dev297 → ha_mcp_dev-7.1.0.dev299}/src/ha_mcp/tools/tools_resources.py +0 -0
- {ha_mcp_dev-7.1.0.dev297 → ha_mcp_dev-7.1.0.dev299}/src/ha_mcp/tools/tools_service.py +0 -0
- {ha_mcp_dev-7.1.0.dev297 → ha_mcp_dev-7.1.0.dev299}/src/ha_mcp/tools/tools_services.py +0 -0
- {ha_mcp_dev-7.1.0.dev297 → ha_mcp_dev-7.1.0.dev299}/src/ha_mcp/tools/tools_system.py +0 -0
- {ha_mcp_dev-7.1.0.dev297 → ha_mcp_dev-7.1.0.dev299}/src/ha_mcp/tools/tools_todo.py +0 -0
- {ha_mcp_dev-7.1.0.dev297 → ha_mcp_dev-7.1.0.dev299}/src/ha_mcp/tools/tools_traces.py +0 -0
- {ha_mcp_dev-7.1.0.dev297 → ha_mcp_dev-7.1.0.dev299}/src/ha_mcp/tools/tools_updates.py +0 -0
- {ha_mcp_dev-7.1.0.dev297 → ha_mcp_dev-7.1.0.dev299}/src/ha_mcp/tools/tools_utility.py +0 -0
- {ha_mcp_dev-7.1.0.dev297 → ha_mcp_dev-7.1.0.dev299}/src/ha_mcp/tools/tools_voice_assistant.py +0 -0
- {ha_mcp_dev-7.1.0.dev297 → ha_mcp_dev-7.1.0.dev299}/src/ha_mcp/tools/tools_zones.py +0 -0
- {ha_mcp_dev-7.1.0.dev297 → ha_mcp_dev-7.1.0.dev299}/src/ha_mcp/tools/util_helpers.py +0 -0
- {ha_mcp_dev-7.1.0.dev297 → ha_mcp_dev-7.1.0.dev299}/src/ha_mcp/transforms/__init__.py +0 -0
- {ha_mcp_dev-7.1.0.dev297 → ha_mcp_dev-7.1.0.dev299}/src/ha_mcp/transforms/categorized_search.py +0 -0
- {ha_mcp_dev-7.1.0.dev297 → ha_mcp_dev-7.1.0.dev299}/src/ha_mcp/utils/__init__.py +0 -0
- {ha_mcp_dev-7.1.0.dev297 → ha_mcp_dev-7.1.0.dev299}/src/ha_mcp/utils/domain_handlers.py +0 -0
- {ha_mcp_dev-7.1.0.dev297 → ha_mcp_dev-7.1.0.dev299}/src/ha_mcp/utils/fuzzy_search.py +0 -0
- {ha_mcp_dev-7.1.0.dev297 → ha_mcp_dev-7.1.0.dev299}/src/ha_mcp/utils/operation_manager.py +0 -0
- {ha_mcp_dev-7.1.0.dev297 → ha_mcp_dev-7.1.0.dev299}/src/ha_mcp/utils/python_sandbox.py +0 -0
- {ha_mcp_dev-7.1.0.dev297 → ha_mcp_dev-7.1.0.dev299}/src/ha_mcp/utils/usage_logger.py +0 -0
- {ha_mcp_dev-7.1.0.dev297 → ha_mcp_dev-7.1.0.dev299}/src/ha_mcp_dev.egg-info/SOURCES.txt +0 -0
- {ha_mcp_dev-7.1.0.dev297 → ha_mcp_dev-7.1.0.dev299}/src/ha_mcp_dev.egg-info/dependency_links.txt +0 -0
- {ha_mcp_dev-7.1.0.dev297 → ha_mcp_dev-7.1.0.dev299}/src/ha_mcp_dev.egg-info/entry_points.txt +0 -0
- {ha_mcp_dev-7.1.0.dev297 → ha_mcp_dev-7.1.0.dev299}/src/ha_mcp_dev.egg-info/requires.txt +0 -0
- {ha_mcp_dev-7.1.0.dev297 → ha_mcp_dev-7.1.0.dev299}/src/ha_mcp_dev.egg-info/top_level.txt +0 -0
- {ha_mcp_dev-7.1.0.dev297 → ha_mcp_dev-7.1.0.dev299}/tests/__init__.py +0 -0
- {ha_mcp_dev-7.1.0.dev297 → ha_mcp_dev-7.1.0.dev299}/tests/test_constants.py +0 -0
- {ha_mcp_dev-7.1.0.dev297 → ha_mcp_dev-7.1.0.dev299}/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.1.0.
|
|
7
|
+
version = "7.1.0.dev299"
|
|
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"
|
|
@@ -24,7 +24,9 @@ BULK_WEBSOCKET_TIMEOUT = 3.0 # Timeout for bulk WebSocket calls
|
|
|
24
24
|
INDIVIDUAL_CONFIG_TIMEOUT = 5.0 # Timeout for individual config fetches
|
|
25
25
|
|
|
26
26
|
# Time budgets for fallback individual fetching (in seconds)
|
|
27
|
-
AUTOMATION_CONFIG_TIME_BUDGET =
|
|
27
|
+
AUTOMATION_CONFIG_TIME_BUDGET = (
|
|
28
|
+
15.0 # Max time for fetching automation configs individually
|
|
29
|
+
)
|
|
28
30
|
SCRIPT_CONFIG_TIME_BUDGET = 10.0 # Max time for fetching script configs individually
|
|
29
31
|
|
|
30
32
|
|
|
@@ -131,7 +133,9 @@ class SmartSearchTools:
|
|
|
131
133
|
}
|
|
132
134
|
|
|
133
135
|
if not matches or (matches and matches[0]["score"] < 80):
|
|
134
|
-
response["suggestions"] = self.fuzzy_searcher.get_smart_suggestions(
|
|
136
|
+
response["suggestions"] = self.fuzzy_searcher.get_smart_suggestions(
|
|
137
|
+
entities, query
|
|
138
|
+
)
|
|
135
139
|
|
|
136
140
|
return response
|
|
137
141
|
|
|
@@ -144,7 +148,11 @@ class SmartSearchTools:
|
|
|
144
148
|
"Verify entity exists with get_all_states",
|
|
145
149
|
"Try simpler search terms",
|
|
146
150
|
],
|
|
147
|
-
context={
|
|
151
|
+
context={
|
|
152
|
+
"query": query,
|
|
153
|
+
"matches": [],
|
|
154
|
+
"error_source": "smart_entity_search",
|
|
155
|
+
},
|
|
148
156
|
)
|
|
149
157
|
|
|
150
158
|
async def get_entities_by_area(
|
|
@@ -613,7 +621,11 @@ class SmartSearchTools:
|
|
|
613
621
|
"Verify API token permissions",
|
|
614
622
|
"Try test_connection first",
|
|
615
623
|
],
|
|
616
|
-
context={
|
|
624
|
+
context={
|
|
625
|
+
"total_entities": 0,
|
|
626
|
+
"entity_summary": {},
|
|
627
|
+
"controllable_devices": {},
|
|
628
|
+
},
|
|
617
629
|
)
|
|
618
630
|
|
|
619
631
|
async def deep_search(
|
|
@@ -624,20 +636,22 @@ class SmartSearchTools:
|
|
|
624
636
|
offset: int = 0,
|
|
625
637
|
include_config: bool = False,
|
|
626
638
|
concurrency_limit: int = DEFAULT_CONCURRENCY_LIMIT,
|
|
639
|
+
exact_match: bool = True,
|
|
627
640
|
) -> dict[str, Any]:
|
|
628
641
|
"""
|
|
629
|
-
Deep search across automation, script, and
|
|
642
|
+
Deep search across automation, script, helper, and dashboard definitions.
|
|
630
643
|
|
|
631
644
|
Searches not just entity names but also within configuration definitions
|
|
632
645
|
including triggers, actions, sequences, and other config fields.
|
|
633
646
|
|
|
634
647
|
Args:
|
|
635
|
-
query: Search query (can be partial, with typos)
|
|
648
|
+
query: Search query (can be partial, with typos when exact_match=False)
|
|
636
649
|
search_types: Types to search (default: ["automation", "script", "helper"])
|
|
637
650
|
limit: Maximum total results to return (default: 5)
|
|
638
651
|
offset: Number of results to skip for pagination (default: 0)
|
|
639
652
|
include_config: Include full config in results (default: False)
|
|
640
653
|
concurrency_limit: Max concurrent API calls for config fetching
|
|
654
|
+
exact_match: Use exact substring matching (default: True). Set False for fuzzy.
|
|
641
655
|
|
|
642
656
|
Returns:
|
|
643
657
|
Dictionary with search results grouped by type
|
|
@@ -650,6 +664,7 @@ class SmartSearchTools:
|
|
|
650
664
|
"automations": [],
|
|
651
665
|
"scripts": [],
|
|
652
666
|
"helpers": [],
|
|
667
|
+
"dashboards": [],
|
|
653
668
|
}
|
|
654
669
|
|
|
655
670
|
query_lower = query.lower().strip()
|
|
@@ -738,7 +753,9 @@ class SmartSearchTools:
|
|
|
738
753
|
all_automation_configs[uid] = item
|
|
739
754
|
bulk_fetched = True
|
|
740
755
|
except Exception as e:
|
|
741
|
-
logger.debug(
|
|
756
|
+
logger.debug(
|
|
757
|
+
f"Automation WebSocket bulk fetch ({ws_type}) failed: {e}"
|
|
758
|
+
)
|
|
742
759
|
|
|
743
760
|
# Attempt C: Individual REST calls with time budget (LAST RESORT)
|
|
744
761
|
# Prioritize name-matched automations so we at least get their configs
|
|
@@ -754,7 +771,10 @@ class SmartSearchTools:
|
|
|
754
771
|
_name_score,
|
|
755
772
|
unique_id,
|
|
756
773
|
) in sorted_by_score:
|
|
757
|
-
if
|
|
774
|
+
if (
|
|
775
|
+
time.perf_counter() - budget_start
|
|
776
|
+
> AUTOMATION_CONFIG_TIME_BUDGET
|
|
777
|
+
):
|
|
758
778
|
break
|
|
759
779
|
if not unique_id or unique_id in all_automation_configs:
|
|
760
780
|
continue
|
|
@@ -767,7 +787,9 @@ class SmartSearchTools:
|
|
|
767
787
|
)
|
|
768
788
|
all_automation_configs[unique_id] = config
|
|
769
789
|
except Exception as e:
|
|
770
|
-
logger.debug(
|
|
790
|
+
logger.debug(
|
|
791
|
+
f"Automation individual config fetch ({unique_id}) failed: {e}"
|
|
792
|
+
)
|
|
771
793
|
|
|
772
794
|
# Phase 3: Score with whatever configs we have
|
|
773
795
|
for entity_id, friendly_name, name_score, unique_id in name_scored:
|
|
@@ -775,20 +797,27 @@ class SmartSearchTools:
|
|
|
775
797
|
all_automation_configs.get(unique_id, {}) if unique_id else {}
|
|
776
798
|
)
|
|
777
799
|
config_match_score = (
|
|
778
|
-
self._search_in_dict(config, query_lower)
|
|
800
|
+
self._search_in_dict(config, query_lower, exact_match)
|
|
801
|
+
if config
|
|
802
|
+
else 0
|
|
803
|
+
)
|
|
804
|
+
total_score, threshold, match_in_name = self._score_deep_match(
|
|
805
|
+
entity_id,
|
|
806
|
+
friendly_name,
|
|
807
|
+
name_score,
|
|
808
|
+
config_match_score,
|
|
809
|
+
query_lower,
|
|
810
|
+
exact_match,
|
|
779
811
|
)
|
|
780
|
-
total_score = max(name_score, config_match_score)
|
|
781
812
|
|
|
782
|
-
if total_score >=
|
|
813
|
+
if total_score >= threshold:
|
|
783
814
|
results["automations"].append(
|
|
784
815
|
{
|
|
785
816
|
"entity_id": entity_id,
|
|
786
817
|
"friendly_name": friendly_name,
|
|
787
818
|
"score": total_score,
|
|
788
|
-
"match_in_name":
|
|
789
|
-
>=
|
|
790
|
-
"match_in_config": config_match_score
|
|
791
|
-
>= self.settings.fuzzy_threshold,
|
|
819
|
+
"match_in_name": match_in_name,
|
|
820
|
+
"match_in_config": config_match_score >= threshold,
|
|
792
821
|
"config": config if config else None,
|
|
793
822
|
}
|
|
794
823
|
)
|
|
@@ -861,7 +890,9 @@ class SmartSearchTools:
|
|
|
861
890
|
all_script_configs[sid] = item
|
|
862
891
|
script_bulk_fetched = True
|
|
863
892
|
except Exception as e:
|
|
864
|
-
logger.debug(
|
|
893
|
+
logger.debug(
|
|
894
|
+
f"Script WebSocket bulk fetch ({ws_type}) failed: {e}"
|
|
895
|
+
)
|
|
865
896
|
|
|
866
897
|
# Attempt C: Individual fetch with budget
|
|
867
898
|
if not script_bulk_fetched:
|
|
@@ -875,7 +906,10 @@ class SmartSearchTools:
|
|
|
875
906
|
script_id,
|
|
876
907
|
_name_score,
|
|
877
908
|
) in sorted_scripts:
|
|
878
|
-
if
|
|
909
|
+
if (
|
|
910
|
+
time.perf_counter() - budget_start
|
|
911
|
+
> SCRIPT_CONFIG_TIME_BUDGET
|
|
912
|
+
):
|
|
879
913
|
break
|
|
880
914
|
if script_id in all_script_configs:
|
|
881
915
|
continue
|
|
@@ -888,7 +922,9 @@ class SmartSearchTools:
|
|
|
888
922
|
"config", {}
|
|
889
923
|
)
|
|
890
924
|
except Exception as e:
|
|
891
|
-
logger.debug(
|
|
925
|
+
logger.debug(
|
|
926
|
+
f"Script individual config fetch ({script_id}) failed: {e}"
|
|
927
|
+
)
|
|
892
928
|
|
|
893
929
|
# Phase 3: Score scripts
|
|
894
930
|
for (
|
|
@@ -899,23 +935,28 @@ class SmartSearchTools:
|
|
|
899
935
|
) in script_name_scored:
|
|
900
936
|
script_config = all_script_configs.get(script_id, {})
|
|
901
937
|
config_match_score = (
|
|
902
|
-
self._search_in_dict(script_config, query_lower)
|
|
938
|
+
self._search_in_dict(script_config, query_lower, exact_match)
|
|
903
939
|
if script_config
|
|
904
940
|
else 0
|
|
905
941
|
)
|
|
906
|
-
total_score =
|
|
942
|
+
total_score, threshold, match_in_name = self._score_deep_match(
|
|
943
|
+
entity_id,
|
|
944
|
+
friendly_name,
|
|
945
|
+
name_score,
|
|
946
|
+
config_match_score,
|
|
947
|
+
query_lower,
|
|
948
|
+
exact_match,
|
|
949
|
+
)
|
|
907
950
|
|
|
908
|
-
if total_score >=
|
|
951
|
+
if total_score >= threshold:
|
|
909
952
|
results["scripts"].append(
|
|
910
953
|
{
|
|
911
954
|
"entity_id": entity_id,
|
|
912
955
|
"script_id": script_id,
|
|
913
956
|
"friendly_name": friendly_name,
|
|
914
957
|
"score": total_score,
|
|
915
|
-
"match_in_name":
|
|
916
|
-
>=
|
|
917
|
-
"match_in_config": config_match_score
|
|
918
|
-
>= self.settings.fuzzy_threshold,
|
|
958
|
+
"match_in_name": match_in_name,
|
|
959
|
+
"match_in_config": config_match_score >= threshold,
|
|
919
960
|
"config": script_config if script_config else None,
|
|
920
961
|
}
|
|
921
962
|
)
|
|
@@ -958,22 +999,29 @@ class SmartSearchTools:
|
|
|
958
999
|
)
|
|
959
1000
|
)
|
|
960
1001
|
config_match_score = self._search_in_dict(
|
|
961
|
-
helper, query_lower
|
|
1002
|
+
helper, query_lower, exact_match
|
|
1003
|
+
)
|
|
1004
|
+
total_score, threshold, match_in_name = (
|
|
1005
|
+
self._score_deep_match(
|
|
1006
|
+
entity_id,
|
|
1007
|
+
name,
|
|
1008
|
+
name_match_score,
|
|
1009
|
+
config_match_score,
|
|
1010
|
+
query_lower,
|
|
1011
|
+
exact_match,
|
|
1012
|
+
)
|
|
962
1013
|
)
|
|
963
1014
|
|
|
964
|
-
total_score
|
|
965
|
-
|
|
966
|
-
if total_score >= self.settings.fuzzy_threshold:
|
|
1015
|
+
if total_score >= threshold:
|
|
967
1016
|
helper_results.append(
|
|
968
1017
|
{
|
|
969
1018
|
"entity_id": entity_id,
|
|
970
1019
|
"helper_type": helper_type,
|
|
971
1020
|
"name": name,
|
|
972
1021
|
"score": total_score,
|
|
973
|
-
"match_in_name":
|
|
974
|
-
>= self.settings.fuzzy_threshold,
|
|
1022
|
+
"match_in_name": match_in_name,
|
|
975
1023
|
"match_in_config": config_match_score
|
|
976
|
-
>=
|
|
1024
|
+
>= threshold,
|
|
977
1025
|
"config": helper,
|
|
978
1026
|
}
|
|
979
1027
|
)
|
|
@@ -996,6 +1044,98 @@ class SmartSearchTools:
|
|
|
996
1044
|
elif isinstance(result, Exception):
|
|
997
1045
|
logger.debug(f"Helper list fetch failed: {result}")
|
|
998
1046
|
|
|
1047
|
+
# ================================================================
|
|
1048
|
+
# DASHBOARD SEARCH
|
|
1049
|
+
# Fetches all storage-mode dashboards and the default dashboard,
|
|
1050
|
+
# then searches their configs (cards, badges, views) for the query.
|
|
1051
|
+
# ================================================================
|
|
1052
|
+
if "dashboard" in search_types:
|
|
1053
|
+
try:
|
|
1054
|
+
# List all storage-mode dashboards
|
|
1055
|
+
dash_list_resp = await self.client.send_websocket_message(
|
|
1056
|
+
{"type": "lovelace/dashboards/list"}
|
|
1057
|
+
)
|
|
1058
|
+
dashboard_entries: list[dict[str, Any]] = []
|
|
1059
|
+
if isinstance(dash_list_resp, dict) and dash_list_resp.get(
|
|
1060
|
+
"success"
|
|
1061
|
+
):
|
|
1062
|
+
dashboard_entries = dash_list_resp.get("result", [])
|
|
1063
|
+
|
|
1064
|
+
# Build list of dashboards to search (include default)
|
|
1065
|
+
dashboards_to_search: list[tuple[str, str]] = [
|
|
1066
|
+
("default", "Default Dashboard")
|
|
1067
|
+
]
|
|
1068
|
+
for dash in dashboard_entries:
|
|
1069
|
+
url_path = dash.get("url_path", "")
|
|
1070
|
+
title = dash.get("title", url_path)
|
|
1071
|
+
if url_path:
|
|
1072
|
+
dashboards_to_search.append((url_path, title))
|
|
1073
|
+
|
|
1074
|
+
async def search_dashboard(
|
|
1075
|
+
url_path: str, title: str
|
|
1076
|
+
) -> list[dict[str, Any]]:
|
|
1077
|
+
"""Search a single dashboard's config for the query."""
|
|
1078
|
+
async with semaphore:
|
|
1079
|
+
try:
|
|
1080
|
+
get_data: dict[str, Any] = {"type": "lovelace/config"}
|
|
1081
|
+
if url_path != "default":
|
|
1082
|
+
get_data["url_path"] = url_path
|
|
1083
|
+
resp = await asyncio.wait_for(
|
|
1084
|
+
self.client.send_websocket_message(get_data),
|
|
1085
|
+
timeout=INDIVIDUAL_CONFIG_TIMEOUT,
|
|
1086
|
+
)
|
|
1087
|
+
config = (
|
|
1088
|
+
resp.get("result", resp)
|
|
1089
|
+
if isinstance(resp, dict)
|
|
1090
|
+
else resp
|
|
1091
|
+
)
|
|
1092
|
+
if not isinstance(config, dict):
|
|
1093
|
+
return []
|
|
1094
|
+
|
|
1095
|
+
# Search the entire dashboard config
|
|
1096
|
+
config_score = self._search_in_dict(
|
|
1097
|
+
config, query_lower, exact_match
|
|
1098
|
+
)
|
|
1099
|
+
threshold = (
|
|
1100
|
+
100
|
|
1101
|
+
if exact_match
|
|
1102
|
+
else self.settings.fuzzy_threshold
|
|
1103
|
+
)
|
|
1104
|
+
if config_score >= threshold:
|
|
1105
|
+
return [
|
|
1106
|
+
{
|
|
1107
|
+
"dashboard_url": url_path,
|
|
1108
|
+
"dashboard_title": title,
|
|
1109
|
+
"score": config_score,
|
|
1110
|
+
"match_in_config": True,
|
|
1111
|
+
"config": config,
|
|
1112
|
+
}
|
|
1113
|
+
]
|
|
1114
|
+
return []
|
|
1115
|
+
except Exception as e:
|
|
1116
|
+
logger.debug(
|
|
1117
|
+
f"Dashboard search failed ({url_path}): {e}"
|
|
1118
|
+
)
|
|
1119
|
+
return []
|
|
1120
|
+
|
|
1121
|
+
# Search all dashboards in parallel
|
|
1122
|
+
dash_results = await asyncio.gather(
|
|
1123
|
+
*[
|
|
1124
|
+
search_dashboard(url_path, title)
|
|
1125
|
+
for url_path, title in dashboards_to_search
|
|
1126
|
+
],
|
|
1127
|
+
return_exceptions=True,
|
|
1128
|
+
)
|
|
1129
|
+
for dash_result in dash_results:
|
|
1130
|
+
if isinstance(dash_result, list):
|
|
1131
|
+
results["dashboards"].extend(dash_result)
|
|
1132
|
+
elif isinstance(dash_result, Exception):
|
|
1133
|
+
logger.debug(f"Dashboard search failed: {dash_result}")
|
|
1134
|
+
|
|
1135
|
+
except Exception as e:
|
|
1136
|
+
logger.error(f"Dashboard search error: {e}")
|
|
1137
|
+
raise
|
|
1138
|
+
|
|
999
1139
|
# Merge all results with their category, sort by score, and paginate
|
|
1000
1140
|
tagged_results: list[tuple[str, dict[str, Any]]] = []
|
|
1001
1141
|
for category, items in results.items():
|
|
@@ -1004,13 +1144,14 @@ class SmartSearchTools:
|
|
|
1004
1144
|
tagged_results.sort(key=lambda x: x[1]["score"], reverse=True)
|
|
1005
1145
|
|
|
1006
1146
|
total_before_pagination = len(tagged_results)
|
|
1007
|
-
paginated = tagged_results[offset:offset + limit]
|
|
1147
|
+
paginated = tagged_results[offset : offset + limit]
|
|
1008
1148
|
|
|
1009
1149
|
# Re-group paginated results by category
|
|
1010
1150
|
final_results: dict[str, list[dict[str, Any]]] = {
|
|
1011
1151
|
"automations": [],
|
|
1012
1152
|
"scripts": [],
|
|
1013
1153
|
"helpers": [],
|
|
1154
|
+
"dashboards": [],
|
|
1014
1155
|
}
|
|
1015
1156
|
for category, item in paginated:
|
|
1016
1157
|
if not include_config:
|
|
@@ -1019,7 +1160,7 @@ class SmartSearchTools:
|
|
|
1019
1160
|
|
|
1020
1161
|
has_more = (offset + len(paginated)) < total_before_pagination
|
|
1021
1162
|
|
|
1022
|
-
|
|
1163
|
+
response: dict[str, Any] = {
|
|
1023
1164
|
"success": True,
|
|
1024
1165
|
"query": query,
|
|
1025
1166
|
"total_matches": total_before_pagination,
|
|
@@ -1034,6 +1175,12 @@ class SmartSearchTools:
|
|
|
1034
1175
|
"search_types": search_types,
|
|
1035
1176
|
}
|
|
1036
1177
|
|
|
1178
|
+
# Only include dashboards key when dashboard search was requested
|
|
1179
|
+
if "dashboard" in search_types:
|
|
1180
|
+
response["dashboards"] = final_results["dashboards"]
|
|
1181
|
+
|
|
1182
|
+
return response
|
|
1183
|
+
|
|
1037
1184
|
except Exception as e:
|
|
1038
1185
|
logger.error(f"Error in deep_search: {e}")
|
|
1039
1186
|
exception_to_structured_error(
|
|
@@ -1043,44 +1190,92 @@ class SmartSearchTools:
|
|
|
1043
1190
|
"Verify automation/script/helper entities exist",
|
|
1044
1191
|
"Try simpler search terms",
|
|
1045
1192
|
],
|
|
1046
|
-
context={
|
|
1193
|
+
context={
|
|
1194
|
+
"query": query,
|
|
1195
|
+
"automations": [],
|
|
1196
|
+
"scripts": [],
|
|
1197
|
+
"helpers": [],
|
|
1198
|
+
},
|
|
1047
1199
|
)
|
|
1048
1200
|
|
|
1201
|
+
def _score_deep_match(
|
|
1202
|
+
self,
|
|
1203
|
+
entity_id: str,
|
|
1204
|
+
friendly_name: str,
|
|
1205
|
+
fuzzy_name_score: int,
|
|
1206
|
+
config_match_score: int,
|
|
1207
|
+
query_lower: str,
|
|
1208
|
+
exact_match: bool,
|
|
1209
|
+
) -> tuple[int, int, bool]:
|
|
1210
|
+
"""Compute total score, threshold, and match_in_name for a deep search result.
|
|
1211
|
+
|
|
1212
|
+
Returns (total_score, threshold, match_in_name).
|
|
1213
|
+
"""
|
|
1214
|
+
if exact_match:
|
|
1215
|
+
name_exact = (
|
|
1216
|
+
100
|
|
1217
|
+
if query_lower in entity_id.lower()
|
|
1218
|
+
or query_lower in friendly_name.lower()
|
|
1219
|
+
else 0
|
|
1220
|
+
)
|
|
1221
|
+
total_score = max(name_exact, config_match_score)
|
|
1222
|
+
return total_score, 100, name_exact >= 100
|
|
1223
|
+
else:
|
|
1224
|
+
total_score = max(fuzzy_name_score, config_match_score)
|
|
1225
|
+
threshold = self.settings.fuzzy_threshold
|
|
1226
|
+
return total_score, threshold, fuzzy_name_score >= threshold
|
|
1227
|
+
|
|
1049
1228
|
def _search_in_dict(
|
|
1050
|
-
self,
|
|
1229
|
+
self,
|
|
1230
|
+
data: dict[str, Any] | list[Any] | Any,
|
|
1231
|
+
query: str,
|
|
1232
|
+
exact_match: bool = False,
|
|
1051
1233
|
) -> int:
|
|
1052
1234
|
"""
|
|
1053
1235
|
Recursively search for query string in nested dictionary/list structures.
|
|
1054
1236
|
|
|
1055
|
-
|
|
1237
|
+
When exact_match is True, uses substring matching (returns 100 if found, 0 if not).
|
|
1238
|
+
When exact_match is False, uses fuzzy matching with partial ratio scoring.
|
|
1056
1239
|
"""
|
|
1057
1240
|
max_score = 0
|
|
1058
1241
|
|
|
1059
1242
|
if isinstance(data, dict):
|
|
1060
1243
|
for key, value in data.items():
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1244
|
+
if exact_match:
|
|
1245
|
+
if query in str(key).lower():
|
|
1246
|
+
return 100
|
|
1247
|
+
else:
|
|
1248
|
+
key_score = calculate_partial_ratio(query, str(key).lower())
|
|
1249
|
+
max_score = max(max_score, key_score)
|
|
1064
1250
|
|
|
1065
|
-
|
|
1066
|
-
value_score = self._search_in_dict(value, query)
|
|
1251
|
+
value_score = self._search_in_dict(value, query, exact_match)
|
|
1067
1252
|
max_score = max(max_score, value_score)
|
|
1253
|
+
if exact_match and max_score >= 100:
|
|
1254
|
+
return 100
|
|
1068
1255
|
|
|
1069
1256
|
elif isinstance(data, list):
|
|
1070
1257
|
for item in data:
|
|
1071
|
-
item_score = self._search_in_dict(item, query)
|
|
1258
|
+
item_score = self._search_in_dict(item, query, exact_match)
|
|
1072
1259
|
max_score = max(max_score, item_score)
|
|
1260
|
+
if exact_match and max_score >= 100:
|
|
1261
|
+
return 100
|
|
1073
1262
|
|
|
1074
1263
|
elif isinstance(data, str):
|
|
1075
|
-
|
|
1076
|
-
|
|
1264
|
+
if exact_match:
|
|
1265
|
+
if query in data.lower():
|
|
1266
|
+
return 100
|
|
1267
|
+
else:
|
|
1268
|
+
max_score = max(max_score, calculate_partial_ratio(query, data.lower()))
|
|
1077
1269
|
|
|
1078
1270
|
elif data is not None:
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1271
|
+
if exact_match:
|
|
1272
|
+
if query in str(data).lower():
|
|
1273
|
+
return 100
|
|
1274
|
+
else:
|
|
1275
|
+
max_score = max(
|
|
1276
|
+
max_score,
|
|
1277
|
+
calculate_partial_ratio(query, str(data).lower()),
|
|
1278
|
+
)
|
|
1084
1279
|
|
|
1085
1280
|
return max_score
|
|
1086
1281
|
|