99import time
1010import uuid
1111from collections .abc import Callable
12- from typing import Any , Type
12+ from typing import Any , Type , TypedDict
1313from warnings import warn
1414
1515import aiohttp
@@ -110,6 +110,21 @@ def send_deprecation_warning(old_name: str, new_name: str) -> None: # pragma: n
110110 _LOGGER .warning (message )
111111
112112
113+ class PublishResponse (TypedDict ):
114+ """Response from a publish operation.
115+
116+ The result field is an integer where 0 indicates success.
117+ """
118+
119+ result : int # 0 = success
120+
121+
122+ class ParseAndShareLocationResponse (TypedDict ):
123+ """Response from parseAndShareLocationToVehicle mutation."""
124+
125+ publishResponse : PublishResponse
126+
127+
113128class Rivian :
114129 """Main class for the Rivian API Client"""
115130
@@ -206,7 +221,7 @@ def _handle_gql_error(self, exception: TransportQueryError) -> None:
206221 Raises:
207222 Appropriate RivianApiException subclass based on error code
208223 """
209- errors = exception .errors if hasattr (exception , ' errors' ) else []
224+ errors = exception .errors if hasattr (exception , " errors" ) else []
210225
211226 for error in errors or []:
212227 if isinstance (error , dict ) and (extensions := error .get ("extensions" )):
@@ -228,7 +243,9 @@ def _handle_gql_error(self, exception: TransportQueryError) -> None:
228243 raise err_cls (str (exception ))
229244
230245 # If no specific error found, raise generic exception
231- raise RivianApiException (f"Error occurred while communicating with Rivian: { exception } " )
246+ raise RivianApiException (
247+ f"Error occurred while communicating with Rivian: { exception } "
248+ )
232249
233250 async def create_csrf_token (self ) -> None :
234251 """Create cross-site-request-forgery (csrf) token."""
@@ -274,12 +291,16 @@ async def authenticate(self, username: str, password: str) -> None:
274291 query = dsl_gql (
275292 DSLMutation (
276293 self ._ds .Mutation .login .args (email = username , password = password ).select (
277- DSLInlineFragment ().on (self ._ds .MobileLoginResponse ).select (
294+ DSLInlineFragment ()
295+ .on (self ._ds .MobileLoginResponse )
296+ .select (
278297 self ._ds .MobileLoginResponse .accessToken ,
279298 self ._ds .MobileLoginResponse .refreshToken ,
280299 self ._ds .MobileLoginResponse .userSessionToken ,
281300 ),
282- DSLInlineFragment ().on (self ._ds .MobileMFALoginResponse ).select (
301+ DSLInlineFragment ()
302+ .on (self ._ds .MobileMFALoginResponse )
303+ .select (
283304 self ._ds .MobileMFALoginResponse .otpToken ,
284305 ),
285306 )
@@ -333,7 +354,9 @@ async def validate_otp(self, username: str, otp_code: str) -> None:
333354 self ._ds .Mutation .loginWithOTP .args (
334355 email = username , otpCode = otp_code , otpToken = self ._otp_token
335356 ).select (
336- DSLInlineFragment ().on (self ._ds .MobileLoginResponse ).select (
357+ DSLInlineFragment ()
358+ .on (self ._ds .MobileLoginResponse )
359+ .select (
337360 self ._ds .MobileLoginResponse .accessToken ,
338361 self ._ds .MobileLoginResponse .refreshToken ,
339362 self ._ds .MobileLoginResponse .userSessionToken ,
@@ -662,7 +685,7 @@ async def get_live_charging_session(
662685 def _validate_vehicle_command (
663686 self , command : VehicleCommand | str , params : dict [str , Any ] | None = None
664687 ) -> None :
665- """Validate certian vehicle command/param combos."""
688+ """Validate certain vehicle command/param combos."""
666689 if command == VehicleCommand .CHARGING_LIMITS :
667690 if not (
668691 params
@@ -779,6 +802,66 @@ async def send_vehicle_command(
779802 command_data = result .get ("sendVehicleCommand" , {})
780803 return command_data .get ("id" )
781804
805+ async def send_location_to_vehicle (
806+ self ,
807+ location_str : str ,
808+ vehicle_id : str ,
809+ ) -> ParseAndShareLocationResponse :
810+ """Send a location/address to the vehicle's navigation system.
811+
812+ This mutation does not require phone enrollment or HMAC signing.
813+ It works via cloud API only and is a "fire-and-forget" operation.
814+ The mutation returns success when the Rivian cloud receives the message,
815+ not when the vehicle actually receives it. The vehicle will pick up the
816+ destination when it next connects to the cloud.
817+
818+ Args:
819+ location_str: Address string or coordinates
820+ Examples: "123 Main St, Springfield, IL 62701"
821+ "40.7128,-74.0060" (latitude,longitude)
822+ vehicle_id: The vehicle ID to send the location to
823+
824+ Returns:
825+ Response dict with publishResponse.result field (int, where 0 = success)
826+
827+ Raises:
828+ RivianApiException: If the request fails
829+ RivianUnauthenticated: If authentication is invalid
830+ RivianBadRequestError: If the location string cannot be parsed
831+ """
832+ client = await self ._ensure_client (GRAPHQL_GATEWAY )
833+ assert self ._ds is not None
834+
835+ # Build DSL mutation
836+ mutation = dsl_gql (
837+ DSLMutation (
838+ self ._ds .Mutation .parseAndShareLocationToVehicle .args (
839+ str = location_str , vehicleId = vehicle_id
840+ ).select (
841+ self ._ds .ParseAndShareLocationToVehicleResponse .publishResponse .select (
842+ self ._ds .PublishResponse .result
843+ )
844+ )
845+ )
846+ )
847+
848+ # Execute mutation with error handling
849+ try :
850+ async with async_timeout .timeout (self .request_timeout ):
851+ result = await client .execute_async (mutation )
852+ except TransportQueryError as exception :
853+ self ._handle_gql_error (exception )
854+ except asyncio .TimeoutError as exception :
855+ raise RivianApiException (
856+ "Timeout occurred while sending location to vehicle."
857+ ) from exception
858+ except Exception as exception :
859+ raise RivianApiException (
860+ "Error occurred while sending location to vehicle."
861+ ) from exception
862+
863+ return result .get ("parseAndShareLocationToVehicle" , {})
864+
782865 async def subscribe_for_vehicle_updates (
783866 self ,
784867 vehicle_id : str ,
0 commit comments