ha-mcp-dev 6.7.2.dev251__tar.gz → 6.7.2.dev253__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.dev251/src/ha_mcp_dev.egg-info → ha_mcp_dev-6.7.2.dev253}/PKG-INFO +1 -1
- {ha_mcp_dev-6.7.2.dev251 → ha_mcp_dev-6.7.2.dev253}/pyproject.toml +1 -1
- {ha_mcp_dev-6.7.2.dev251 → ha_mcp_dev-6.7.2.dev253}/src/ha_mcp/client/rest_client.py +8 -4
- {ha_mcp_dev-6.7.2.dev251 → ha_mcp_dev-6.7.2.dev253}/src/ha_mcp/client/websocket_client.py +105 -23
- {ha_mcp_dev-6.7.2.dev251 → ha_mcp_dev-6.7.2.dev253/src/ha_mcp_dev.egg-info}/PKG-INFO +1 -1
- {ha_mcp_dev-6.7.2.dev251 → ha_mcp_dev-6.7.2.dev253}/LICENSE +0 -0
- {ha_mcp_dev-6.7.2.dev251 → ha_mcp_dev-6.7.2.dev253}/MANIFEST.in +0 -0
- {ha_mcp_dev-6.7.2.dev251 → ha_mcp_dev-6.7.2.dev253}/README.md +0 -0
- {ha_mcp_dev-6.7.2.dev251 → ha_mcp_dev-6.7.2.dev253}/setup.cfg +0 -0
- {ha_mcp_dev-6.7.2.dev251 → ha_mcp_dev-6.7.2.dev253}/src/ha_mcp/__init__.py +0 -0
- {ha_mcp_dev-6.7.2.dev251 → ha_mcp_dev-6.7.2.dev253}/src/ha_mcp/__main__.py +0 -0
- {ha_mcp_dev-6.7.2.dev251 → ha_mcp_dev-6.7.2.dev253}/src/ha_mcp/_pypi_marker +0 -0
- {ha_mcp_dev-6.7.2.dev251 → ha_mcp_dev-6.7.2.dev253}/src/ha_mcp/auth/__init__.py +0 -0
- {ha_mcp_dev-6.7.2.dev251 → ha_mcp_dev-6.7.2.dev253}/src/ha_mcp/auth/consent_form.py +0 -0
- {ha_mcp_dev-6.7.2.dev251 → ha_mcp_dev-6.7.2.dev253}/src/ha_mcp/auth/provider.py +0 -0
- {ha_mcp_dev-6.7.2.dev251 → ha_mcp_dev-6.7.2.dev253}/src/ha_mcp/client/__init__.py +0 -0
- {ha_mcp_dev-6.7.2.dev251 → ha_mcp_dev-6.7.2.dev253}/src/ha_mcp/client/websocket_listener.py +0 -0
- {ha_mcp_dev-6.7.2.dev251 → ha_mcp_dev-6.7.2.dev253}/src/ha_mcp/config.py +0 -0
- {ha_mcp_dev-6.7.2.dev251 → ha_mcp_dev-6.7.2.dev253}/src/ha_mcp/errors.py +0 -0
- {ha_mcp_dev-6.7.2.dev251 → ha_mcp_dev-6.7.2.dev253}/src/ha_mcp/py.typed +0 -0
- {ha_mcp_dev-6.7.2.dev251 → ha_mcp_dev-6.7.2.dev253}/src/ha_mcp/resources/card_types.json +0 -0
- {ha_mcp_dev-6.7.2.dev251 → ha_mcp_dev-6.7.2.dev253}/src/ha_mcp/resources/dashboard_guide.md +0 -0
- {ha_mcp_dev-6.7.2.dev251 → ha_mcp_dev-6.7.2.dev253}/src/ha_mcp/resources/skills-vendor/.claude/settings.json +0 -0
- {ha_mcp_dev-6.7.2.dev251 → ha_mcp_dev-6.7.2.dev253}/src/ha_mcp/resources/skills-vendor/.claude-plugin/marketplace.json +0 -0
- {ha_mcp_dev-6.7.2.dev251 → ha_mcp_dev-6.7.2.dev253}/src/ha_mcp/resources/skills-vendor/.claude-plugin/plugin.json +0 -0
- {ha_mcp_dev-6.7.2.dev251 → ha_mcp_dev-6.7.2.dev253}/src/ha_mcp/resources/skills-vendor/.github/ISSUE_TEMPLATE/skill-rca.md +0 -0
- {ha_mcp_dev-6.7.2.dev251 → ha_mcp_dev-6.7.2.dev253}/src/ha_mcp/resources/skills-vendor/AGENTS.md +0 -0
- {ha_mcp_dev-6.7.2.dev251 → ha_mcp_dev-6.7.2.dev253}/src/ha_mcp/resources/skills-vendor/CLAUDE.md +0 -0
- {ha_mcp_dev-6.7.2.dev251 → ha_mcp_dev-6.7.2.dev253}/src/ha_mcp/resources/skills-vendor/CONTRIBUTING.md +0 -0
- {ha_mcp_dev-6.7.2.dev251 → ha_mcp_dev-6.7.2.dev253}/src/ha_mcp/resources/skills-vendor/LICENSE +0 -0
- {ha_mcp_dev-6.7.2.dev251 → ha_mcp_dev-6.7.2.dev253}/src/ha_mcp/resources/skills-vendor/README.md +0 -0
- {ha_mcp_dev-6.7.2.dev251 → ha_mcp_dev-6.7.2.dev253}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/SKILL.md +0 -0
- {ha_mcp_dev-6.7.2.dev251 → ha_mcp_dev-6.7.2.dev253}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/automation-patterns.md +0 -0
- {ha_mcp_dev-6.7.2.dev251 → ha_mcp_dev-6.7.2.dev253}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/device-control.md +0 -0
- {ha_mcp_dev-6.7.2.dev251 → ha_mcp_dev-6.7.2.dev253}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/examples.yaml +0 -0
- {ha_mcp_dev-6.7.2.dev251 → ha_mcp_dev-6.7.2.dev253}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/helper-selection.md +0 -0
- {ha_mcp_dev-6.7.2.dev251 → ha_mcp_dev-6.7.2.dev253}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/safe-refactoring.md +0 -0
- {ha_mcp_dev-6.7.2.dev251 → ha_mcp_dev-6.7.2.dev253}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/template-guidelines.md +0 -0
- {ha_mcp_dev-6.7.2.dev251 → ha_mcp_dev-6.7.2.dev253}/src/ha_mcp/server.py +0 -0
- {ha_mcp_dev-6.7.2.dev251 → ha_mcp_dev-6.7.2.dev253}/src/ha_mcp/smoke_test.py +0 -0
- {ha_mcp_dev-6.7.2.dev251 → ha_mcp_dev-6.7.2.dev253}/src/ha_mcp/tools/__init__.py +0 -0
- {ha_mcp_dev-6.7.2.dev251 → ha_mcp_dev-6.7.2.dev253}/src/ha_mcp/tools/backup.py +0 -0
- {ha_mcp_dev-6.7.2.dev251 → ha_mcp_dev-6.7.2.dev253}/src/ha_mcp/tools/device_control.py +0 -0
- {ha_mcp_dev-6.7.2.dev251 → ha_mcp_dev-6.7.2.dev253}/src/ha_mcp/tools/enhanced.py +0 -0
- {ha_mcp_dev-6.7.2.dev251 → ha_mcp_dev-6.7.2.dev253}/src/ha_mcp/tools/helpers.py +0 -0
- {ha_mcp_dev-6.7.2.dev251 → ha_mcp_dev-6.7.2.dev253}/src/ha_mcp/tools/registry.py +0 -0
- {ha_mcp_dev-6.7.2.dev251 → ha_mcp_dev-6.7.2.dev253}/src/ha_mcp/tools/smart_search.py +0 -0
- {ha_mcp_dev-6.7.2.dev251 → ha_mcp_dev-6.7.2.dev253}/src/ha_mcp/tools/tools_addons.py +0 -0
- {ha_mcp_dev-6.7.2.dev251 → ha_mcp_dev-6.7.2.dev253}/src/ha_mcp/tools/tools_areas.py +0 -0
- {ha_mcp_dev-6.7.2.dev251 → ha_mcp_dev-6.7.2.dev253}/src/ha_mcp/tools/tools_blueprints.py +0 -0
- {ha_mcp_dev-6.7.2.dev251 → ha_mcp_dev-6.7.2.dev253}/src/ha_mcp/tools/tools_bug_report.py +0 -0
- {ha_mcp_dev-6.7.2.dev251 → ha_mcp_dev-6.7.2.dev253}/src/ha_mcp/tools/tools_calendar.py +0 -0
- {ha_mcp_dev-6.7.2.dev251 → ha_mcp_dev-6.7.2.dev253}/src/ha_mcp/tools/tools_camera.py +0 -0
- {ha_mcp_dev-6.7.2.dev251 → ha_mcp_dev-6.7.2.dev253}/src/ha_mcp/tools/tools_config_automations.py +0 -0
- {ha_mcp_dev-6.7.2.dev251 → ha_mcp_dev-6.7.2.dev253}/src/ha_mcp/tools/tools_config_dashboards.py +0 -0
- {ha_mcp_dev-6.7.2.dev251 → ha_mcp_dev-6.7.2.dev253}/src/ha_mcp/tools/tools_config_entry_flow.py +0 -0
- {ha_mcp_dev-6.7.2.dev251 → ha_mcp_dev-6.7.2.dev253}/src/ha_mcp/tools/tools_config_helpers.py +0 -0
- {ha_mcp_dev-6.7.2.dev251 → ha_mcp_dev-6.7.2.dev253}/src/ha_mcp/tools/tools_config_info.py +0 -0
- {ha_mcp_dev-6.7.2.dev251 → ha_mcp_dev-6.7.2.dev253}/src/ha_mcp/tools/tools_config_scripts.py +0 -0
- {ha_mcp_dev-6.7.2.dev251 → ha_mcp_dev-6.7.2.dev253}/src/ha_mcp/tools/tools_entities.py +0 -0
- {ha_mcp_dev-6.7.2.dev251 → ha_mcp_dev-6.7.2.dev253}/src/ha_mcp/tools/tools_filesystem.py +0 -0
- {ha_mcp_dev-6.7.2.dev251 → ha_mcp_dev-6.7.2.dev253}/src/ha_mcp/tools/tools_groups.py +0 -0
- {ha_mcp_dev-6.7.2.dev251 → ha_mcp_dev-6.7.2.dev253}/src/ha_mcp/tools/tools_hacs.py +0 -0
- {ha_mcp_dev-6.7.2.dev251 → ha_mcp_dev-6.7.2.dev253}/src/ha_mcp/tools/tools_history.py +0 -0
- {ha_mcp_dev-6.7.2.dev251 → ha_mcp_dev-6.7.2.dev253}/src/ha_mcp/tools/tools_integrations.py +0 -0
- {ha_mcp_dev-6.7.2.dev251 → ha_mcp_dev-6.7.2.dev253}/src/ha_mcp/tools/tools_labels.py +0 -0
- {ha_mcp_dev-6.7.2.dev251 → ha_mcp_dev-6.7.2.dev253}/src/ha_mcp/tools/tools_mcp_component.py +0 -0
- {ha_mcp_dev-6.7.2.dev251 → ha_mcp_dev-6.7.2.dev253}/src/ha_mcp/tools/tools_registry.py +0 -0
- {ha_mcp_dev-6.7.2.dev251 → ha_mcp_dev-6.7.2.dev253}/src/ha_mcp/tools/tools_resources.py +0 -0
- {ha_mcp_dev-6.7.2.dev251 → ha_mcp_dev-6.7.2.dev253}/src/ha_mcp/tools/tools_search.py +0 -0
- {ha_mcp_dev-6.7.2.dev251 → ha_mcp_dev-6.7.2.dev253}/src/ha_mcp/tools/tools_service.py +0 -0
- {ha_mcp_dev-6.7.2.dev251 → ha_mcp_dev-6.7.2.dev253}/src/ha_mcp/tools/tools_services.py +0 -0
- {ha_mcp_dev-6.7.2.dev251 → ha_mcp_dev-6.7.2.dev253}/src/ha_mcp/tools/tools_system.py +0 -0
- {ha_mcp_dev-6.7.2.dev251 → ha_mcp_dev-6.7.2.dev253}/src/ha_mcp/tools/tools_todo.py +0 -0
- {ha_mcp_dev-6.7.2.dev251 → ha_mcp_dev-6.7.2.dev253}/src/ha_mcp/tools/tools_traces.py +0 -0
- {ha_mcp_dev-6.7.2.dev251 → ha_mcp_dev-6.7.2.dev253}/src/ha_mcp/tools/tools_updates.py +0 -0
- {ha_mcp_dev-6.7.2.dev251 → ha_mcp_dev-6.7.2.dev253}/src/ha_mcp/tools/tools_utility.py +0 -0
- {ha_mcp_dev-6.7.2.dev251 → ha_mcp_dev-6.7.2.dev253}/src/ha_mcp/tools/tools_voice_assistant.py +0 -0
- {ha_mcp_dev-6.7.2.dev251 → ha_mcp_dev-6.7.2.dev253}/src/ha_mcp/tools/tools_zones.py +0 -0
- {ha_mcp_dev-6.7.2.dev251 → ha_mcp_dev-6.7.2.dev253}/src/ha_mcp/tools/util_helpers.py +0 -0
- {ha_mcp_dev-6.7.2.dev251 → ha_mcp_dev-6.7.2.dev253}/src/ha_mcp/utils/__init__.py +0 -0
- {ha_mcp_dev-6.7.2.dev251 → ha_mcp_dev-6.7.2.dev253}/src/ha_mcp/utils/domain_handlers.py +0 -0
- {ha_mcp_dev-6.7.2.dev251 → ha_mcp_dev-6.7.2.dev253}/src/ha_mcp/utils/fuzzy_search.py +0 -0
- {ha_mcp_dev-6.7.2.dev251 → ha_mcp_dev-6.7.2.dev253}/src/ha_mcp/utils/operation_manager.py +0 -0
- {ha_mcp_dev-6.7.2.dev251 → ha_mcp_dev-6.7.2.dev253}/src/ha_mcp/utils/python_sandbox.py +0 -0
- {ha_mcp_dev-6.7.2.dev251 → ha_mcp_dev-6.7.2.dev253}/src/ha_mcp/utils/usage_logger.py +0 -0
- {ha_mcp_dev-6.7.2.dev251 → ha_mcp_dev-6.7.2.dev253}/src/ha_mcp_dev.egg-info/SOURCES.txt +0 -0
- {ha_mcp_dev-6.7.2.dev251 → ha_mcp_dev-6.7.2.dev253}/src/ha_mcp_dev.egg-info/dependency_links.txt +0 -0
- {ha_mcp_dev-6.7.2.dev251 → ha_mcp_dev-6.7.2.dev253}/src/ha_mcp_dev.egg-info/entry_points.txt +0 -0
- {ha_mcp_dev-6.7.2.dev251 → ha_mcp_dev-6.7.2.dev253}/src/ha_mcp_dev.egg-info/requires.txt +0 -0
- {ha_mcp_dev-6.7.2.dev251 → ha_mcp_dev-6.7.2.dev253}/src/ha_mcp_dev.egg-info/top_level.txt +0 -0
- {ha_mcp_dev-6.7.2.dev251 → ha_mcp_dev-6.7.2.dev253}/tests/__init__.py +0 -0
- {ha_mcp_dev-6.7.2.dev251 → ha_mcp_dev-6.7.2.dev253}/tests/test_constants.py +0 -0
- {ha_mcp_dev-6.7.2.dev251 → ha_mcp_dev-6.7.2.dev253}/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.dev253"
|
|
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"
|
|
@@ -684,8 +684,10 @@ class HomeAssistantClient:
|
|
|
684
684
|
async def send_websocket_message(self, message: dict[str, Any]) -> dict[str, Any]:
|
|
685
685
|
"""Send message via WebSocket and wait for response.
|
|
686
686
|
|
|
687
|
-
Uses
|
|
688
|
-
|
|
687
|
+
Uses a per-client WebSocket connection keyed to the client's own
|
|
688
|
+
credentials (base_url + token). This ensures OAuth mode uses the
|
|
689
|
+
real HA credentials from the token claims, not the global sentinel
|
|
690
|
+
settings.
|
|
689
691
|
"""
|
|
690
692
|
from .websocket_client import get_websocket_client
|
|
691
693
|
|
|
@@ -694,8 +696,10 @@ class HomeAssistantClient:
|
|
|
694
696
|
|
|
695
697
|
for attempt in range(max_retries):
|
|
696
698
|
try:
|
|
697
|
-
# Use
|
|
698
|
-
ws_client = await get_websocket_client(
|
|
699
|
+
# Use per-client WebSocket keyed to this client's credentials
|
|
700
|
+
ws_client = await get_websocket_client(
|
|
701
|
+
url=self.base_url, token=self.token
|
|
702
|
+
)
|
|
699
703
|
|
|
700
704
|
# Special handling for render_template which returns an event with the actual result
|
|
701
705
|
if message.get("type") == "render_template":
|
|
@@ -8,6 +8,7 @@ This module handles WebSocket connections to Home Assistant for:
|
|
|
8
8
|
"""
|
|
9
9
|
|
|
10
10
|
import asyncio
|
|
11
|
+
import hashlib
|
|
11
12
|
import json
|
|
12
13
|
import logging
|
|
13
14
|
import time
|
|
@@ -553,11 +554,21 @@ class HomeAssistantWebSocketClient:
|
|
|
553
554
|
|
|
554
555
|
|
|
555
556
|
|
|
557
|
+
MAX_POOL_SIZE = 50
|
|
558
|
+
|
|
559
|
+
|
|
556
560
|
class WebSocketManager:
|
|
557
|
-
"""Singleton manager for Home Assistant WebSocket connections.
|
|
561
|
+
"""Singleton manager for Home Assistant WebSocket connections.
|
|
562
|
+
|
|
563
|
+
Maintains a pool of WebSocket connections keyed by (url, token) so that
|
|
564
|
+
multiple OAuth users can have concurrent connections without interfering
|
|
565
|
+
with each other. The pool is bounded to ``MAX_POOL_SIZE`` entries; when
|
|
566
|
+
this limit is exceeded the least-recently-used connection is evicted.
|
|
567
|
+
"""
|
|
558
568
|
|
|
559
569
|
_instance = None
|
|
560
|
-
|
|
570
|
+
_clients: dict[str, HomeAssistantWebSocketClient]
|
|
571
|
+
_last_used: dict[str, float]
|
|
561
572
|
_current_loop: asyncio.AbstractEventLoop | None = None
|
|
562
573
|
_lock: asyncio.Lock | None = None
|
|
563
574
|
_lock_loop: asyncio.AbstractEventLoop | None = None
|
|
@@ -566,6 +577,8 @@ class WebSocketManager:
|
|
|
566
577
|
def __new__(cls) -> "WebSocketManager":
|
|
567
578
|
if cls._instance is None:
|
|
568
579
|
cls._instance = super().__new__(cls)
|
|
580
|
+
cls._instance._clients = {}
|
|
581
|
+
cls._instance._last_used = {}
|
|
569
582
|
cls._instance._lock = None
|
|
570
583
|
cls._instance._lock_loop = None
|
|
571
584
|
cls._instance._client_factory = HomeAssistantWebSocketClient
|
|
@@ -600,8 +613,28 @@ class WebSocketManager:
|
|
|
600
613
|
self._lock_loop = current_loop
|
|
601
614
|
logger.debug("Created new WebSocketManager lock for current event loop")
|
|
602
615
|
|
|
603
|
-
|
|
604
|
-
|
|
616
|
+
@staticmethod
|
|
617
|
+
def _client_key(url: str, token: str) -> str:
|
|
618
|
+
"""Create a cache key from credentials."""
|
|
619
|
+
return hashlib.sha256(f"{url.rstrip('/')}:{token}".encode()).hexdigest()
|
|
620
|
+
|
|
621
|
+
async def get_client(
|
|
622
|
+
self,
|
|
623
|
+
url: str | None = None,
|
|
624
|
+
token: str | None = None,
|
|
625
|
+
) -> HomeAssistantWebSocketClient:
|
|
626
|
+
"""Get WebSocket client, creating connection if needed.
|
|
627
|
+
|
|
628
|
+
Maintains a pool of connections keyed by credentials. In OAuth mode,
|
|
629
|
+
each user gets their own connection. In non-OAuth mode, the global
|
|
630
|
+
settings are used as the key.
|
|
631
|
+
|
|
632
|
+
Args:
|
|
633
|
+
url: Optional HA URL. If provided with token, uses these
|
|
634
|
+
credentials instead of global settings. This is required
|
|
635
|
+
for OAuth mode where each request has its own credentials.
|
|
636
|
+
token: Optional HA token. Must be provided with url.
|
|
637
|
+
"""
|
|
605
638
|
current_loop = asyncio.get_event_loop()
|
|
606
639
|
|
|
607
640
|
self._ensure_lock()
|
|
@@ -610,47 +643,96 @@ class WebSocketManager:
|
|
|
610
643
|
raise Exception("Lock not initialized")
|
|
611
644
|
async with self._lock:
|
|
612
645
|
if self._current_loop is not None and self._current_loop != current_loop:
|
|
613
|
-
|
|
646
|
+
# Event loop changed — disconnect all clients
|
|
647
|
+
for client in self._clients.values():
|
|
614
648
|
try:
|
|
615
|
-
await
|
|
649
|
+
await client.disconnect()
|
|
616
650
|
except Exception:
|
|
617
651
|
pass
|
|
618
|
-
|
|
652
|
+
self._clients.clear()
|
|
653
|
+
self._last_used.clear()
|
|
619
654
|
|
|
620
655
|
self._current_loop = current_loop
|
|
621
656
|
|
|
622
|
-
|
|
623
|
-
|
|
657
|
+
# Determine credentials to use
|
|
658
|
+
if url and token:
|
|
659
|
+
ws_url = url
|
|
660
|
+
ws_token = token
|
|
661
|
+
else:
|
|
662
|
+
settings = get_global_settings()
|
|
663
|
+
ws_url = settings.homeassistant_url
|
|
664
|
+
ws_token = settings.homeassistant_token
|
|
665
|
+
|
|
666
|
+
key = self._client_key(ws_url, ws_token)
|
|
667
|
+
|
|
668
|
+
# Return existing connected client for these credentials
|
|
669
|
+
existing = self._clients.get(key)
|
|
670
|
+
if existing and existing.is_connected:
|
|
671
|
+
self._last_used[key] = time.monotonic()
|
|
672
|
+
return existing
|
|
673
|
+
|
|
674
|
+
# Remove stale client if present
|
|
675
|
+
if existing:
|
|
676
|
+
self._clients.pop(key, None)
|
|
677
|
+
self._last_used.pop(key, None)
|
|
624
678
|
|
|
625
|
-
settings = get_global_settings()
|
|
626
679
|
factory = self._client_factory or HomeAssistantWebSocketClient
|
|
627
|
-
|
|
628
|
-
settings.homeassistant_url, settings.homeassistant_token
|
|
629
|
-
)
|
|
680
|
+
client = factory(ws_url, ws_token)
|
|
630
681
|
|
|
631
|
-
connected = await
|
|
682
|
+
connected = await client.connect()
|
|
632
683
|
if not connected:
|
|
633
684
|
raise Exception("Failed to connect to Home Assistant WebSocket")
|
|
634
685
|
|
|
635
|
-
|
|
686
|
+
self._clients[key] = client
|
|
687
|
+
self._last_used[key] = time.monotonic()
|
|
688
|
+
|
|
689
|
+
# Evict least-recently-used connection if over limit
|
|
690
|
+
if len(self._clients) > MAX_POOL_SIZE:
|
|
691
|
+
oldest_key = min(self._last_used, key=self._last_used.get)
|
|
692
|
+
stale = self._clients.pop(oldest_key, None)
|
|
693
|
+
self._last_used.pop(oldest_key, None)
|
|
694
|
+
if stale:
|
|
695
|
+
try:
|
|
696
|
+
await stale.disconnect()
|
|
697
|
+
except Exception:
|
|
698
|
+
logger.warning(
|
|
699
|
+
"Error disconnecting evicted WebSocket client",
|
|
700
|
+
exc_info=True,
|
|
701
|
+
)
|
|
702
|
+
|
|
703
|
+
return client
|
|
636
704
|
|
|
637
705
|
async def disconnect(self) -> None:
|
|
638
|
-
"""Disconnect WebSocket
|
|
706
|
+
"""Disconnect all WebSocket clients."""
|
|
639
707
|
self._ensure_lock()
|
|
640
708
|
|
|
641
709
|
if not self._lock:
|
|
642
710
|
raise Exception("Lock not initialized")
|
|
643
711
|
async with self._lock:
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
712
|
+
for client in self._clients.values():
|
|
713
|
+
try:
|
|
714
|
+
await client.disconnect()
|
|
715
|
+
except Exception:
|
|
716
|
+
logger.warning(
|
|
717
|
+
"Error disconnecting WebSocket client", exc_info=True
|
|
718
|
+
)
|
|
719
|
+
self._clients.clear()
|
|
720
|
+
self._last_used.clear()
|
|
721
|
+
self._current_loop = None
|
|
648
722
|
|
|
649
723
|
|
|
650
724
|
# Global WebSocket manager instance
|
|
651
725
|
websocket_manager = WebSocketManager()
|
|
652
726
|
|
|
653
727
|
|
|
654
|
-
async def get_websocket_client(
|
|
655
|
-
|
|
656
|
-
|
|
728
|
+
async def get_websocket_client(
|
|
729
|
+
url: str | None = None,
|
|
730
|
+
token: str | None = None,
|
|
731
|
+
) -> HomeAssistantWebSocketClient:
|
|
732
|
+
"""Get the global WebSocket client instance.
|
|
733
|
+
|
|
734
|
+
Args:
|
|
735
|
+
url: Optional HA URL for per-client credentials (OAuth mode).
|
|
736
|
+
token: Optional HA token for per-client credentials (OAuth mode).
|
|
737
|
+
"""
|
|
738
|
+
return await websocket_manager.get_client(url=url, token=token)
|
|
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.dev251 → ha_mcp_dev-6.7.2.dev253}/src/ha_mcp/resources/skills-vendor/AGENTS.md
RENAMED
|
File without changes
|
{ha_mcp_dev-6.7.2.dev251 → ha_mcp_dev-6.7.2.dev253}/src/ha_mcp/resources/skills-vendor/CLAUDE.md
RENAMED
|
File without changes
|
|
File without changes
|
{ha_mcp_dev-6.7.2.dev251 → ha_mcp_dev-6.7.2.dev253}/src/ha_mcp/resources/skills-vendor/LICENSE
RENAMED
|
File without changes
|
{ha_mcp_dev-6.7.2.dev251 → ha_mcp_dev-6.7.2.dev253}/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.dev251 → ha_mcp_dev-6.7.2.dev253}/src/ha_mcp/tools/tools_config_automations.py
RENAMED
|
File without changes
|
{ha_mcp_dev-6.7.2.dev251 → ha_mcp_dev-6.7.2.dev253}/src/ha_mcp/tools/tools_config_dashboards.py
RENAMED
|
File without changes
|
{ha_mcp_dev-6.7.2.dev251 → ha_mcp_dev-6.7.2.dev253}/src/ha_mcp/tools/tools_config_entry_flow.py
RENAMED
|
File without changes
|
{ha_mcp_dev-6.7.2.dev251 → ha_mcp_dev-6.7.2.dev253}/src/ha_mcp/tools/tools_config_helpers.py
RENAMED
|
File without changes
|
|
File without changes
|
{ha_mcp_dev-6.7.2.dev251 → ha_mcp_dev-6.7.2.dev253}/src/ha_mcp/tools/tools_config_scripts.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{ha_mcp_dev-6.7.2.dev251 → ha_mcp_dev-6.7.2.dev253}/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.dev251 → ha_mcp_dev-6.7.2.dev253}/src/ha_mcp_dev.egg-info/dependency_links.txt
RENAMED
|
File without changes
|
{ha_mcp_dev-6.7.2.dev251 → ha_mcp_dev-6.7.2.dev253}/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
|