diff --git a/homeassistant/components/sandbox/entity/calendar.py b/homeassistant/components/sandbox/entity/calendar.py index 3edf6c989117..b93230964e8e 100644 --- a/homeassistant/components/sandbox/entity/calendar.py +++ b/homeassistant/components/sandbox/entity/calendar.py @@ -5,7 +5,7 @@ from typing import Any from homeassistant.components.calendar import CalendarEntity, CalendarEvent -from . import SandboxProxyEntity, raise_not_proxied +from . import SandboxProxyEntity def _parse_calendar_date(value: Any) -> datetime.date | datetime.datetime | Any: @@ -52,11 +52,11 @@ class SandboxCalendarEntity(SandboxProxyEntity, CalendarEntity): ``create_event`` forwards through the standard ``calendar.create_event`` service. The listing query (``async_get_events``) rides the - ``calendar.get_events`` ``SupportsResponse`` service. The WS-only event - edits (``calendar/event/update`` / ``delete``) need the request/response - ``EntityQuery`` RPC; until that lands they raise. The recurrence-timer - subscription (``calendar/event/subscribe``) is deferred — the - next/current event is not pushed, so ``event`` returns ``None``. See + ``calendar.get_events`` ``SupportsResponse`` service; the WS-only event + edits (``calendar/event/update`` / ``delete``) cross via the generic + ``EntityQuery`` RPC. The recurrence-timer subscription + (``calendar/event/subscribe``) is deferred — the next/current event is not + pushed, so ``event`` returns ``None``. See ``sandbox/docs/query-shaped-rpcs.md``. """ @@ -92,8 +92,14 @@ class SandboxCalendarEntity(SandboxProxyEntity, CalendarEntity): recurrence_id: str | None = None, recurrence_range: str | None = None, ) -> None: - """Raise — ``calendar/event/update`` needs the EntityQuery RPC.""" - raise_not_proxied("Updating a calendar event") + """Forward the WS-only event update through ``EntityQuery``.""" + await self._entity_query( + "async_update_event", + uid=uid, + event=event, + recurrence_id=recurrence_id, + recurrence_range=recurrence_range, + ) async def async_delete_event( self, @@ -101,5 +107,10 @@ class SandboxCalendarEntity(SandboxProxyEntity, CalendarEntity): recurrence_id: str | None = None, recurrence_range: str | None = None, ) -> None: - """Raise — ``calendar/event/delete`` needs the EntityQuery RPC.""" - raise_not_proxied("Deleting a calendar event") + """Forward the WS-only event delete through ``EntityQuery``.""" + await self._entity_query( + "async_delete_event", + uid=uid, + recurrence_id=recurrence_id, + recurrence_range=recurrence_range, + ) diff --git a/homeassistant/components/sandbox/entity/media_player.py b/homeassistant/components/sandbox/entity/media_player.py index 1e4b1fb89fb4..c7ce91a3b610 100644 --- a/homeassistant/components/sandbox/entity/media_player.py +++ b/homeassistant/components/sandbox/entity/media_player.py @@ -31,7 +31,7 @@ from homeassistant.components.media_player import ( ) from homeassistant.exceptions import HomeAssistantError -from . import SandboxProxyEntity, raise_not_proxied +from . import SandboxProxyEntity if TYPE_CHECKING: from ..bridge import SandboxBridge, SandboxEntityDescription @@ -64,6 +64,18 @@ def _browse_media_from_dict(data: dict[str, Any]) -> BrowseMedia: ) +def _search_media_from_dict(data: dict[str, Any]) -> SearchMedia: + """Rebuild a :class:`SearchMedia` from its ``as_dict`` shape. + + ``SearchMedia.as_dict`` holds its results under ``result`` as a list of + ``BrowseMedia`` dicts, so the rebuild reuses :func:`_browse_media_from_dict` + per item. ``version`` is constructor-defaulted. + """ + return SearchMedia( + result=[_browse_media_from_dict(item) for item in data.get("result", [])] + ) + + # pylint: disable-next=home-assistant-enforce-class-module class SandboxMediaPlayerEntity(SandboxProxyEntity, MediaPlayerEntity): """Proxy for a ``media_player`` entity in a sandbox.""" @@ -276,8 +288,24 @@ class SandboxMediaPlayerEntity(SandboxProxyEntity, MediaPlayerEntity): return _browse_media_from_dict(entity_response) async def async_search_media(self, query: SearchMediaQuery) -> SearchMedia: - """Raise — media search is a server-side query, not yet proxied.""" - raise_not_proxied("Searching media") + """Search via ``EntityQuery`` against the real entity. + + Forwarded to ``async_internal_search_media`` (which rebuilds the + ``SearchMediaQuery`` from flat kwargs on the sandbox side) rather than + ``async_search_media``, so the query crosses as plain JSON kwargs. + ``media_filter_classes`` cross as their ``MediaClass`` string values. + """ + args: dict[str, Any] = {"search_query": query.search_query} + if query.media_content_type is not None: + args["media_content_type"] = query.media_content_type + if query.media_content_id is not None: + args["media_content_id"] = query.media_content_id + if query.media_filter_classes is not None: + args["media_filter_classes"] = [ + getattr(item, "value", item) for item in query.media_filter_classes + ] + response = await self._entity_query("async_internal_search_media", **args) + return _search_media_from_dict(response or {}) async def async_clear_playlist(self) -> None: """Forward clear_playlist.""" diff --git a/homeassistant/components/sandbox/entity/update.py b/homeassistant/components/sandbox/entity/update.py index 15b2221f90b4..674c2f569818 100644 --- a/homeassistant/components/sandbox/entity/update.py +++ b/homeassistant/components/sandbox/entity/update.py @@ -9,7 +9,7 @@ from homeassistant.components.update import ( UpdateEntityFeature, ) -from . import SandboxProxyEntity, raise_not_proxied +from . import SandboxProxyEntity if TYPE_CHECKING: from ..bridge import SandboxBridge, SandboxEntityDescription @@ -99,5 +99,5 @@ class SandboxUpdateEntity(SandboxProxyEntity, UpdateEntity): await self._call_service("install", **payload) async def async_release_notes(self) -> str | None: - """Raise — ``update/release_notes`` is a WS query, not yet proxied.""" - raise_not_proxied("Fetching update release notes") + """Return the release notes via ``EntityQuery`` (a plain str/None).""" + return await self._entity_query("async_release_notes") diff --git a/homeassistant/components/sandbox/entity/vacuum.py b/homeassistant/components/sandbox/entity/vacuum.py index b41cab41808b..51cebced7f2c 100644 --- a/homeassistant/components/sandbox/entity/vacuum.py +++ b/homeassistant/components/sandbox/entity/vacuum.py @@ -11,12 +11,17 @@ from homeassistant.components.vacuum import ( VacuumEntityFeature, ) -from . import SandboxProxyEntity, raise_not_proxied +from . import SandboxProxyEntity if TYPE_CHECKING: from ..bridge import SandboxBridge, SandboxEntityDescription +def _segment_from_dict(data: dict[str, Any]) -> Segment: + """Rebuild a :class:`Segment` dataclass from its serialised dict.""" + return Segment(id=data["id"], name=data["name"], group=data.get("group")) + + # pylint: disable-next=home-assistant-enforce-class-module class SandboxVacuumEntity(SandboxProxyEntity, StateVacuumEntity): """Proxy for a ``vacuum`` entity in a sandbox.""" @@ -94,5 +99,6 @@ class SandboxVacuumEntity(SandboxProxyEntity, StateVacuumEntity): await self._call_service("send_command", **payload) async def async_get_segments(self) -> list[Segment]: - """Raise — ``vacuum/get_segments`` is a WS query, not yet proxied.""" - raise_not_proxied("Listing vacuum segments") + """Return the cleanable segments via ``EntityQuery``.""" + response = await self._entity_query("async_get_segments") + return [_segment_from_dict(segment) for segment in response or []]