Skip to content

Commit 19ff068

Browse files
committed
Add entity registry methods and models for WebSocket clients
New models for entity registry responses (EntityRegistryEntry, EntityRegistryEntryExtended, EntityRegistryUpdateResult) and list/get/update/remove methods on both sync and async WS clients. Also adds configurable max_size param to WS client init (default 16MB) to handle large responses like full entity registry lists. Update CI Python version from 3.9 to 3.11.
1 parent a7030f3 commit 19ff068

6 files changed

Lines changed: 218 additions & 17 deletions

File tree

.github/workflows/test-suite.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,15 +21,15 @@ jobs:
2121
- name: Setup Python
2222
uses: actions/setup-python@v3
2323
with:
24-
python-version: "3.9"
24+
python-version: "3.11"
2525
- name: Checkout
2626
uses: actions/checkout@v3
2727
with:
2828
ref: ${{ github.event.pull_request.head.sha }}
2929
- name: Install uv
3030
uses: astral-sh/setup-uv@v4
3131
- name: Install Dependencies
32-
run: uv sync --group styling
32+
run: uv sync --group dev
3333
- name: Run Ruff format
3434
run: uv run ruff format homeassistant_api
3535
- name: Run Ruff linting

homeassistant_api/asyncwebsocket.py

Lines changed: 64 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,9 @@
2525
from homeassistant_api.models import State
2626
from homeassistant_api.models.config_entries import DisableEnableResult
2727
from homeassistant_api.models.config_entries import FlowResult
28+
from homeassistant_api.models.entity_registry import EntityRegistryEntry
29+
from homeassistant_api.models.entity_registry import EntityRegistryEntryExtended
30+
from homeassistant_api.models.entity_registry import EntityRegistryUpdateResult
2831
from homeassistant_api.models.states import Context
2932
from homeassistant_api.models.websocket import AuthInvalid
3033
from homeassistant_api.models.websocket import AuthOk
@@ -48,12 +51,12 @@
4851
class AsyncWebsocketClient(BaseWebsocketClient):
4952
_async_conn: ws.ClientConnection | None
5053

51-
def __init__(self, api_url: str, token: str) -> None:
52-
super().__init__(api_url, token)
54+
def __init__(self, api_url: str, token: str, *, max_size: int = 2**24) -> None:
55+
super().__init__(api_url, token, max_size=max_size)
5356
self._async_conn = None
5457

5558
async def __aenter__(self) -> Self:
56-
self._async_conn = await ws.connect(self.api_url)
59+
self._async_conn = await ws.connect(self.api_url, max_size=self.max_size)
5760
await self._async_conn.__aenter__()
5861
okay = await self.authentication_phase()
5962
logger.info("Authenticated with Home Assistant (%s)", okay.ha_version)
@@ -665,6 +668,64 @@ async def delete_entry_subentry(self, entry_id: str, subentry_id: str) -> None:
665668
),
666669
)
667670

671+
# ── Entity Registry ─────────────────────────────────────────
672+
673+
async def list_entity_registry(self) -> tuple[EntityRegistryEntry, ...]:
674+
"""
675+
List all entity registry entries.
676+
677+
Sends command :code:`{"type": "config/entity_registry/list", ...}`.
678+
"""
679+
return tuple(
680+
EntityRegistryEntry.from_json(entry)
681+
for entry in await self.recv_result_list(
682+
await self.send("config/entity_registry/list"),
683+
)
684+
)
685+
686+
async def get_entity_registry_entry(
687+
self,
688+
entity_id: str,
689+
) -> EntityRegistryEntryExtended:
690+
"""
691+
Get a single entity registry entry.
692+
693+
Sends command :code:`{"type": "config/entity_registry/get", ...}`.
694+
"""
695+
result = await self.recv_result_dict(
696+
await self.send("config/entity_registry/get", entity_id=entity_id),
697+
)
698+
return EntityRegistryEntryExtended.from_json(result)
699+
700+
async def update_entity_registry_entry(
701+
self,
702+
entity_id: str,
703+
**kwargs: Any,
704+
) -> EntityRegistryUpdateResult:
705+
"""
706+
Update an entity registry entry.
707+
708+
Sends command :code:`{"type": "config/entity_registry/update", ...}`.
709+
"""
710+
result = await self.recv_result_dict(
711+
await self.send(
712+
"config/entity_registry/update",
713+
entity_id=entity_id,
714+
**kwargs,
715+
),
716+
)
717+
return EntityRegistryUpdateResult.from_json(result)
718+
719+
async def remove_entity_registry_entry(self, entity_id: str) -> None:
720+
"""
721+
Remove an entity from the entity registry.
722+
723+
Sends command :code:`{"type": "config/entity_registry/remove", ...}`.
724+
"""
725+
await self.recv(
726+
await self.send("config/entity_registry/remove", entity_id=entity_id),
727+
)
728+
668729
@contextlib.asynccontextmanager
669730
async def listen_config_entries(
670731
self,

homeassistant_api/basewebsocket.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,18 +21,20 @@ class BaseWebsocketClient:
2121

2222
api_url: str
2323
token: str
24+
max_size: int
2425
_id_counter: int
2526
_result_responses: dict[int, ResultResponse | None]
2627
_event_responses: dict[int, list[EventResponse]]
2728
_ping_responses: dict[int, PingResponse]
2829

29-
def __init__(self, api_url: str, token: str) -> None:
30+
def __init__(self, api_url: str, token: str, *, max_size: int = 2**24) -> None:
3031
parsed = urlparse.urlparse(api_url)
3132
if parsed.scheme not in {"ws", "wss"}:
3233
msg = f"Unknown scheme {parsed.scheme} in {api_url}"
3334
raise ValueError(msg)
3435
self.api_url = api_url
3536
self.token = token.strip()
37+
self.max_size = max_size
3638

3739
self._id_counter = 0
3840
self._result_responses = {} # id -> response

homeassistant_api/models/__init__.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,12 @@
2727
from .entity import BaseGroup
2828
from .entity import Entity
2929
from .entity import Group
30+
from .entity_registry import EntityCategory
31+
from .entity_registry import EntityDisabledBy
32+
from .entity_registry import EntityHiddenBy
33+
from .entity_registry import EntityRegistryEntry
34+
from .entity_registry import EntityRegistryEntryExtended
35+
from .entity_registry import EntityRegistryUpdateResult
3036
from .events import AsyncEvent
3137
from .events import BaseEvent
3238
from .events import Event
@@ -57,6 +63,12 @@
5763
"DiscoveryKey",
5864
"Domain",
5965
"Entity",
66+
"EntityCategory",
67+
"EntityDisabledBy",
68+
"EntityHiddenBy",
69+
"EntityRegistryEntry",
70+
"EntityRegistryEntryExtended",
71+
"EntityRegistryUpdateResult",
6072
"Event",
6173
"FlowContext",
6274
"FlowResult",
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
"""Models for Home Assistant entity registry responses."""
2+
3+
from enum import Enum
4+
from typing import Any
5+
6+
from pydantic import Field
7+
8+
from .base import BaseModel
9+
from .base import DatetimeIsoField
10+
11+
12+
class EntityDisabledBy(str, Enum):
13+
"""What disabled an entity."""
14+
15+
CONFIG_ENTRY = "config_entry"
16+
DEVICE = "device"
17+
HASS = "hass"
18+
INTEGRATION = "integration"
19+
USER = "user"
20+
21+
22+
class EntityHiddenBy(str, Enum):
23+
"""What hid an entity."""
24+
25+
INTEGRATION = "integration"
26+
USER = "user"
27+
28+
29+
class EntityCategory(str, Enum):
30+
"""Category of an entity."""
31+
32+
CONFIG = "config"
33+
DIAGNOSTIC = "diagnostic"
34+
35+
36+
class EntityRegistryEntry(BaseModel):
37+
"""An entity registry entry as returned by ``config/entity_registry/list``."""
38+
39+
area_id: str | None = None
40+
categories: dict[str, str] = Field(default_factory=dict)
41+
config_entry_id: str | None = None
42+
config_subentry_id: str | None = None
43+
created_at: DatetimeIsoField
44+
device_id: str | None = None
45+
disabled_by: EntityDisabledBy | None = None
46+
entity_category: EntityCategory | None = None
47+
entity_id: str
48+
has_entity_name: bool
49+
hidden_by: EntityHiddenBy | None = None
50+
icon: str | None = None
51+
id: str
52+
modified_at: DatetimeIsoField
53+
name: str | None = None
54+
options: dict[str, Any] = Field(default_factory=dict)
55+
original_name: str | None = None
56+
platform: str
57+
translation_key: str | None = None
58+
unique_id: str
59+
60+
61+
class EntityRegistryEntryExtended(EntityRegistryEntry):
62+
"""Extended entity registry entry as returned by ``config/entity_registry/get`` and ``update``."""
63+
64+
aliases: list[str] = Field(default_factory=list)
65+
capabilities: dict[str, Any] | None = None
66+
device_class: str | None = None
67+
original_device_class: str | None = None
68+
original_icon: str | None = None
69+
70+
71+
class EntityRegistryUpdateResult(BaseModel):
72+
"""Result from ``config/entity_registry/update``."""
73+
74+
entity_entry: EntityRegistryEntryExtended
75+
reload_delay: int | None = None
76+
require_restart: bool = False

homeassistant_api/websocket.py

Lines changed: 61 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,9 @@
2525
from homeassistant_api.models import State
2626
from homeassistant_api.models.config_entries import DisableEnableResult
2727
from homeassistant_api.models.config_entries import FlowResult
28+
from homeassistant_api.models.entity_registry import EntityRegistryEntry
29+
from homeassistant_api.models.entity_registry import EntityRegistryEntryExtended
30+
from homeassistant_api.models.entity_registry import EntityRegistryUpdateResult
2831
from homeassistant_api.models.states import Context
2932
from homeassistant_api.models.websocket import AuthInvalid
3033
from homeassistant_api.models.websocket import AuthOk
@@ -48,23 +51,15 @@
4851
class WebsocketClient(BaseWebsocketClient):
4952
_conn: ws.ClientConnection | None
5053

51-
def __init__(self, api_url: str, token: str) -> None:
52-
super().__init__(api_url, token)
54+
def __init__(self, api_url: str, token: str, *, max_size: int = 2**24) -> None:
55+
super().__init__(api_url, token, max_size=max_size)
5356
self._conn = None
5457

55-
self._id_counter = 0
56-
self._result_responses: dict[int, ResultResponse | None] = {} # id -> response
57-
self._event_responses: dict[
58-
int,
59-
list[EventResponse],
60-
] = {} # id -> [response, ...]
61-
self._ping_responses: dict[int, PingResponse] = {} # id -> (sent, received)
62-
6358
def __repr__(self) -> str:
6459
return f"{self.__class__.__name__}({self.api_url!r})"
6560

6661
def __enter__(self) -> Self:
67-
self._conn = ws.connect(self.api_url)
62+
self._conn = ws.connect(self.api_url, max_size=self.max_size)
6863
self._conn.__enter__()
6964
okay = self.authentication_phase()
7065
logger.info("Authenticated with Home Assistant (%s)", okay.ha_version)
@@ -649,6 +644,61 @@ def delete_entry_subentry(self, entry_id: str, subentry_id: str) -> None:
649644
),
650645
)
651646

647+
# ── Entity Registry ─────────────────────────────────────────
648+
649+
def list_entity_registry(self) -> tuple[EntityRegistryEntry, ...]:
650+
"""
651+
List all entity registry entries.
652+
653+
Sends command :code:`{"type": "config/entity_registry/list", ...}`.
654+
"""
655+
return tuple(
656+
EntityRegistryEntry.from_json(entry)
657+
for entry in self.recv_result_list(
658+
self.send("config/entity_registry/list"),
659+
)
660+
)
661+
662+
def get_entity_registry_entry(self, entity_id: str) -> EntityRegistryEntryExtended:
663+
"""
664+
Get a single entity registry entry.
665+
666+
Sends command :code:`{"type": "config/entity_registry/get", ...}`.
667+
"""
668+
result = self.recv_result_dict(
669+
self.send("config/entity_registry/get", entity_id=entity_id),
670+
)
671+
return EntityRegistryEntryExtended.from_json(result)
672+
673+
def update_entity_registry_entry(
674+
self,
675+
entity_id: str,
676+
**kwargs: Any,
677+
) -> EntityRegistryUpdateResult:
678+
"""
679+
Update an entity registry entry.
680+
681+
Sends command :code:`{"type": "config/entity_registry/update", ...}`.
682+
"""
683+
result = self.recv_result_dict(
684+
self.send(
685+
"config/entity_registry/update",
686+
entity_id=entity_id,
687+
**kwargs,
688+
),
689+
)
690+
return EntityRegistryUpdateResult.from_json(result)
691+
692+
def remove_entity_registry_entry(self, entity_id: str) -> None:
693+
"""
694+
Remove an entity from the entity registry.
695+
696+
Sends command :code:`{"type": "config/entity_registry/remove", ...}`.
697+
"""
698+
self.recv(
699+
self.send("config/entity_registry/remove", entity_id=entity_id),
700+
)
701+
652702
@contextlib.contextmanager
653703
def listen_config_entries(
654704
self,

0 commit comments

Comments
 (0)