From a60511db8a3bd1f57693ba38cd20cc5cb5a1d805 Mon Sep 17 00:00:00 2001 From: Antigravity Agent Date: Mon, 26 Oct 2026 17:45:53 -0500 Subject: [PATCH] fix: iterate device_registry.devices directly for HA 2027.9+ compatibility (v3.1.1) --- .../manifest.json | 2 +- .../google_assistant_entity_console/mcp.py | 10 ++- .../google_assistant_entity_console/views.py | 10 ++- docs/ARCHITECTURE.md | 6 +- tests/test_views.py | 80 +++++++++++++++++++ 5 files changed, 99 insertions(+), 9 deletions(-) diff --git a/custom_components/google_assistant_entity_console/manifest.json b/custom_components/google_assistant_entity_console/manifest.json index 7ae1a31..1cf43b9 100644 --- a/custom_components/google_assistant_entity_console/manifest.json +++ b/custom_components/google_assistant_entity_console/manifest.json @@ -8,5 +8,5 @@ "iot_class": "local_polling", "config_flow": true, "requirements": [], - "version": "3.1.0" + "version": "3.1.1" } diff --git a/custom_components/google_assistant_entity_console/mcp.py b/custom_components/google_assistant_entity_console/mcp.py index 862a175..23192b8 100644 --- a/custom_components/google_assistant_entity_console/mcp.py +++ b/custom_components/google_assistant_entity_console/mcp.py @@ -36,7 +36,7 @@ MCP_PROTOCOL_VERSION = "2026-07-28" SERVER_NAME = "google-assistant-entity-console" -SERVER_VERSION = "3.1.0" +SERVER_VERSION = "3.1.1" TOOL_CACHE_TTL_MS = 300000 TOOL_DEFINITIONS: list[dict[str, Any]] = [ @@ -257,8 +257,12 @@ def _get_blocked_entities( if getattr(f, "floor_id", getattr(f, "id", None)) } - devices_dict = getattr(dev_reg, "devices", {}) - device_vals = devices_dict.values() if isinstance(devices_dict, dict) else [] + devices_collection = getattr(dev_reg, "devices", ()) + device_vals = ( + devices_collection.values() + if isinstance(devices_collection, dict) + else devices_collection + ) device_area_map = {d.id: d.area_id for d in device_vals if hasattr(d, "id")} device_name_map = { d.id: getattr(d, "name_by_user", None) or getattr(d, "name", "") diff --git a/custom_components/google_assistant_entity_console/views.py b/custom_components/google_assistant_entity_console/views.py index a08d47f..adaae53 100644 --- a/custom_components/google_assistant_entity_console/views.py +++ b/custom_components/google_assistant_entity_console/views.py @@ -243,10 +243,16 @@ async def async_fetch_entities_data(hass: HomeAssistant): if store.synced_entities: yaml_exposed = store.synced_entities - # Build device mappings + # Build device mappings (iterate directly over dev_reg.devices for HA 2027.9+ compatibility, + # with fallback to .values() when dev_reg.devices is a plain dict in unit tests) device_area_map = {} device_name_map = {} - for device in dev_reg.devices.values(): + devices_iter = ( + dev_reg.devices.values() + if isinstance(dev_reg.devices, dict) + else dev_reg.devices + ) + for device in devices_iter: device_area_map[device.id] = device.area_id device_name_map[device.id] = device.name_by_user or device.name diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index aa47027..994ed50 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -1,4 +1,4 @@ -# Google Assistant Entity Console Architecture (v3.1.0) +# Google Assistant Entity Console Architecture (v3.1.1) This document provides a technical overview of the **Google Assistant Entity Console** Home Assistant custom component architecture. @@ -57,7 +57,7 @@ The integration entrypoint is defined in [`custom_components/google_assistant_en ### Setup Sequence: 1. **Async Setup Entry** (`async_setup_entry`): - - Reads integration manifest version (`3.1.0`). + - Reads integration manifest version (`3.1.1`). - Initializes `ConsoleStore` via `async_get_store(hass)` to load or migrate credentials, blocklists, and sync state from `.storage/google_assistant_entity_console`. - Initializes `GoogleAssistantRuntimeManager` via `async_get_runtime(hass)` and hooks in-memory `GoogleConfig._config["entity_config"]`. - Registers static HTTP paths (`/google_assistant_entity_console/static`) pointing to the component's `static/` directory. @@ -92,7 +92,7 @@ Starting in v3.1.0, Google Assistant Entity Console provides a native MCP server - **Transport & Lifecycle**: - [`McpStreamableView`](file:///home/steve/Repos/GoogleAssistantEntityConsole/custom_components/google_assistant_entity_console/views.py) listens on `POST /api/google_assistant_entity_console/mcp` requiring standard Home Assistant Bearer token authentication (`requires_auth = True`). - Supports stateless JSON-RPC 2.0 request/response cycles as well as `Accept: text/event-stream` SSE streams. - - Implements the MCP initialization handshake (`initialize` returning server capabilities, protocol version `2026-07-28`, and server metadata `google-assistant-entity-console` v3.1.0; `notifications/initialized`). + - Implements the MCP initialization handshake (`initialize` returning server capabilities, protocol version `2026-07-28`, and server metadata `google-assistant-entity-console` v3.1.1; `notifications/initialized`). - Implements JSON-RPC ping (`ping`) and notification handling (`notifications/*` returning HTTP 204 No Content). - Validates protocol headers and includes `Mcp-Protocol-Version: 2026-07-28` in all HTTP responses. - **Tool Registry (`tools/list` & `tools/call`)**: diff --git a/tests/test_views.py b/tests/test_views.py index 28b4037..132c561 100644 --- a/tests/test_views.py +++ b/tests/test_views.py @@ -1707,3 +1707,83 @@ async def test_migrate_yaml_view_post(mock_hass, tmp_path): assert data["project_id"] == "migrated-proj" +@pytest.mark.anyio +async def test_fetch_entities_iterates_device_registry_view_without_values(mock_hass): + """Verifies HA 2027.9+ compatibility where dev_reg.devices is iterated directly without calling .values().""" + + class NonMappingDevicesView: + def __init__(self, items): + self._items = items + + def __iter__(self): + return iter(self._items) + + def values(self): + raise AssertionError("dev_reg.devices.values() is deprecated in HA 2027.9+") + + mock_dev = MagicMock() + mock_dev.id = "dev_2027" + mock_dev.area_id = "area_office" + mock_dev.name_by_user = "Office Desk Lamp" + mock_dev.name = "Desk Lamp" + + mock_dev_reg = MagicMock() + mock_dev_reg.devices = NonMappingDevicesView([mock_dev]) + + mock_entry = MagicMock() + mock_entry.entity_id = "light.office_desk" + mock_entry.disabled_by = None + mock_entry.hidden_by = None + mock_entry.device_id = "dev_2027" + mock_entry.device_class = None + mock_entry.original_device_class = None + mock_entry.area_id = None + mock_entry.name = None + mock_entry.original_name = "Desk" + mock_entry.aliases = [] + mock_entry.options = {} + mock_entry.platform = "hue" + + mock_ent_reg = MagicMock() + mock_ent_reg.entities = {"light.office_desk": mock_entry} + + mock_area = MagicMock() + mock_area.id = "area_office" + mock_area.name = "Office" + mock_area.floor_id = None + mock_area_reg = MagicMock() + mock_area_reg.async_list_areas.return_value = [mock_area] + + with ( + patch( + "custom_components.google_assistant_entity_console.views.entity_registry.async_get", + return_value=mock_ent_reg, + ), + patch( + "custom_components.google_assistant_entity_console.views.device_registry.async_get", + return_value=mock_dev_reg, + ), + patch( + "custom_components.google_assistant_entity_console.views.area_registry.async_get", + return_value=mock_area_reg, + ), + patch( + "custom_components.google_assistant_entity_console.views.floor_registry.async_get", + return_value=MagicMock(async_list_floors=MagicMock(return_value=[])), + ), + patch( + "custom_components.google_assistant_entity_console.views.get_current_yaml_filename", + return_value="dummy.yaml", + ), + patch( + "custom_components.google_assistant_entity_console.views.load_yaml_exposed_entities", + return_value={}, + ), + ): + entities = await async_fetch_entities_data(mock_hass) + assert len(entities) == 1 + assert entities[0]["area"] == "Office" + assert entities[0]["display_name"] == "Office Desk Lamp" + + +