Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -8,5 +8,5 @@
"iot_class": "local_polling",
"config_flow": true,
"requirements": [],
"version": "3.1.0"
"version": "3.1.1"
}
10 changes: 7 additions & 3 deletions custom_components/google_assistant_entity_console/mcp.py
Original file line number Diff line number Diff line change
Expand Up @@ -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]] = [
Expand Down Expand Up @@ -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", "")
Expand Down
10 changes: 8 additions & 2 deletions custom_components/google_assistant_entity_console/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
6 changes: 3 additions & 3 deletions docs/ARCHITECTURE.md
Original file line number Diff line number Diff line change
@@ -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.

Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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`)**:
Expand Down
80 changes: 80 additions & 0 deletions tests/test_views.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"



Loading