From dfebd7eba88370ef5e90982b3d99c1fc332c38ea Mon Sep 17 00:00:00 2001 From: Geoffrey Kruse <6468053+doggkruse@users.noreply.github.com> Date: Sun, 17 Sep 2023 10:23:02 -0700 Subject: [PATCH 01/11] Add initial implementation of BLE key pairing for testing --- pyproject.toml | 3 +- src/rivian/rivian.py | 5 +++ src/rivian/rivian_ble.py | 85 ++++++++++++++++++++++++++++++++++++++++ src/rivian/utils.py | 6 +++ 4 files changed, 98 insertions(+), 1 deletion(-) create mode 100644 src/rivian/rivian_ble.py diff --git a/pyproject.toml b/pyproject.toml index 98cd43d..e69f50a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -10,10 +10,11 @@ packages = [ ] [tool.poetry.dependencies] -python = "^3.9" +python = ">=3.9, <3.13" aiohttp = ">=3.0.0" cryptography = "^41.0.1" backports-strenum = { version = "^1.2.4", python = "<3.11" } +bleak = "^0.21.1" [tool.poetry.group.dev.dependencies] pytest = "^7.1.2" diff --git a/src/rivian/rivian.py b/src/rivian/rivian.py index 0ebf3ff..a1da283 100644 --- a/src/rivian/rivian.py +++ b/src/rivian/rivian.py @@ -28,6 +28,7 @@ ) from .utils import generate_vehicle_command_hmac from .ws_monitor import WebSocketMonitor +from .rivian_ble import pair_phone _LOGGER = logging.getLogger(__name__) @@ -278,6 +279,10 @@ async def enroll_phone( if data.get("data", {}).get("enrollPhone", {}).get("success"): return True return False + + async def ble_pair_phone(self, vehicle_id, phone_id, vehicle_key, private_key): + """Pair a phone key via BLE.""" + await pair_phone(vehicle_id, phone_id, vehicle_key, private_key) async def get_drivers_and_keys(self, vehicle_id: str) -> ClientResponse: """Get drivers and keys.""" diff --git a/src/rivian/rivian_ble.py b/src/rivian/rivian_ble.py new file mode 100644 index 0000000..b29f612 --- /dev/null +++ b/src/rivian/rivian_ble.py @@ -0,0 +1,85 @@ +import platform +import asyncio +import secrets +from asyncio import TimeoutError +from bleak import BleakScanner, BleakClient, BleakError +from rivian import utils + +RIVIAN_PHONE_ID_VEHICLE_ID_UUID = "AA49565A-4D4F-424B-4559-5F5752495445" +RIVIAN_PNONCE_VNONCE_UUID = "E020A15D-E730-4B2C-908B-51DAF9D41E19" +RIVIAN_BLE_ACTIVE_ENTRY_UUID = "5249565F-4D4F-424B-4559-5F5752495445" +RIVIAN_PHONE_KEY_LOCAL_NAME = "Rivian Phone Key" + +# Create an asyncio.Event object to signal the arrival of a new notification. +notification_event = asyncio.Event() +notification_data = None + +def notification_handler(sender, data): + global notification_data + notification_data = data + notification_event.set() + +async def scan_for_device(target_device_name): + while True: + # Sleep before the next scan + await asyncio.sleep(1) + + devices = await BleakScanner.discover() + for device in devices: + if device.name is not None and target_device_name in device.name: + print(f"Found Device: {device.name}, Address: {device.address}") + return device.address + +async def connect_to_device(address): + global client + print(f"Connecting to {address}") + + try: + client = BleakClient(address, timeout=10.0) + await client.connect() + print(f"Connected: {client.is_connected}") + + return client.is_connected + except (BleakError, TimeoutError, OSError): + print(f"Failed to connect to {address}. Retrying...") + return False + +async def pair_phone(vehicle_id, phone_id, vehicle_key, private_key): + while True: + address = await scan_for_device(RIVIAN_PHONE_KEY_LOCAL_NAME) + success = await connect_to_device(address) + + if success: + await client.start_notify(RIVIAN_PHONE_ID_VEHICLE_ID_UUID, notification_handler) + await client.start_notify(RIVIAN_PNONCE_VNONCE_UUID, notification_handler) + # wait to enable notifications for RIVIAN_BLE_ACTIVE_ENTRY_UUID + + # write the phone ID (16-bytes) response will be vehicle ID + await client.write_gatt_char(RIVIAN_PHONE_ID_VEHICLE_ID_UUID, bytes.fromhex(phone_id.replace("-", ""))) + await notification_event.wait() + notification_event.clear() + + # todo check vehicle id + + # generate pnonce (16-bytes random) + pnonce = secrets.token_bytes(16) + hmac = utils.generate_ble_command_hmac(pnonce, vehicle_key, private_key) + + # write pnonce (48-bytes) response will be vnonce + await client.write_gatt_char(RIVIAN_PNONCE_VNONCE_UUID, pnonce + hmac ) + await notification_event.wait() + + # vehicle is authenticated, trigger bonding + if platform.system() == "Darwin": + # Mac BLE API doesn't have an explicit way to trigger bonding + # enable notification on RIVIAN_BLE_ACTIVE_ENTRY_UUID to trigger bonding + await client.start_notify(RIVIAN_BLE_ACTIVE_ENTRY_UUID, notification_handler) + else: + await client.pair() + + if success: + # todo check other steps above + print("Successfully connected and paired") + break + else: + print("Retrying...") diff --git a/src/rivian/utils.py b/src/rivian/utils.py index 7c09c62..ccee44e 100644 --- a/src/rivian/utils.py +++ b/src/rivian/utils.py @@ -66,6 +66,12 @@ def generate_key_pair() -> tuple[str, str]: # Return the public-private key pair as strings return (public_key_str, private_key_str) +def generate_ble_command_hmac( + hmac_data: bytes, vehicle_key: str, private_key: str + ): + """Generate ble command hmac.""" + secret_key = get_secret_key(private_key, vehicle_key) + return bytes.fromhex(get_message_signature(secret_key, hmac_data)) def generate_vehicle_command_hmac( command: str, timestamp: str, vehicle_key: str, private_key: str From ce6fb801cdf113a6e2d5599595b2a75b5ac2c2bb Mon Sep 17 00:00:00 2001 From: Geoffrey Kruse <6468053+doggkruse@users.noreply.github.com> Date: Tue, 19 Sep 2023 09:08:48 -0700 Subject: [PATCH 02/11] Remove RIVIAN prefix on constants per feedback on #38 --- src/rivian/rivian_ble.py | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/src/rivian/rivian_ble.py b/src/rivian/rivian_ble.py index b29f612..576a96e 100644 --- a/src/rivian/rivian_ble.py +++ b/src/rivian/rivian_ble.py @@ -5,10 +5,10 @@ from bleak import BleakScanner, BleakClient, BleakError from rivian import utils -RIVIAN_PHONE_ID_VEHICLE_ID_UUID = "AA49565A-4D4F-424B-4559-5F5752495445" -RIVIAN_PNONCE_VNONCE_UUID = "E020A15D-E730-4B2C-908B-51DAF9D41E19" -RIVIAN_BLE_ACTIVE_ENTRY_UUID = "5249565F-4D4F-424B-4559-5F5752495445" -RIVIAN_PHONE_KEY_LOCAL_NAME = "Rivian Phone Key" +PHONE_ID_VEHICLE_ID_UUID = "AA49565A-4D4F-424B-4559-5F5752495445" +PNONCE_VNONCE_UUID = "E020A15D-E730-4B2C-908B-51DAF9D41E19" +BLE_ACTIVE_ENTRY_UUID = "5249565F-4D4F-424B-4559-5F5752495445" +PHONE_KEY_LOCAL_NAME = "Rivian Phone Key" # Create an asyncio.Event object to signal the arrival of a new notification. notification_event = asyncio.Event() @@ -46,16 +46,16 @@ async def connect_to_device(address): async def pair_phone(vehicle_id, phone_id, vehicle_key, private_key): while True: - address = await scan_for_device(RIVIAN_PHONE_KEY_LOCAL_NAME) + address = await scan_for_device(PHONE_KEY_LOCAL_NAME) success = await connect_to_device(address) if success: - await client.start_notify(RIVIAN_PHONE_ID_VEHICLE_ID_UUID, notification_handler) - await client.start_notify(RIVIAN_PNONCE_VNONCE_UUID, notification_handler) - # wait to enable notifications for RIVIAN_BLE_ACTIVE_ENTRY_UUID + await client.start_notify(PHONE_ID_VEHICLE_ID_UUID, notification_handler) + await client.start_notify(PNONCE_VNONCE_UUID, notification_handler) + # wait to enable notifications for BLE_ACTIVE_ENTRY_UUID # write the phone ID (16-bytes) response will be vehicle ID - await client.write_gatt_char(RIVIAN_PHONE_ID_VEHICLE_ID_UUID, bytes.fromhex(phone_id.replace("-", ""))) + await client.write_gatt_char(PHONE_ID_VEHICLE_ID_UUID, bytes.fromhex(phone_id.replace("-", ""))) await notification_event.wait() notification_event.clear() @@ -66,14 +66,14 @@ async def pair_phone(vehicle_id, phone_id, vehicle_key, private_key): hmac = utils.generate_ble_command_hmac(pnonce, vehicle_key, private_key) # write pnonce (48-bytes) response will be vnonce - await client.write_gatt_char(RIVIAN_PNONCE_VNONCE_UUID, pnonce + hmac ) + await client.write_gatt_char(PNONCE_VNONCE_UUID, pnonce + hmac ) await notification_event.wait() # vehicle is authenticated, trigger bonding if platform.system() == "Darwin": # Mac BLE API doesn't have an explicit way to trigger bonding - # enable notification on RIVIAN_BLE_ACTIVE_ENTRY_UUID to trigger bonding - await client.start_notify(RIVIAN_BLE_ACTIVE_ENTRY_UUID, notification_handler) + # enable notification on BLE_ACTIVE_ENTRY_UUID to trigger bonding + await client.start_notify(BLE_ACTIVE_ENTRY_UUID, notification_handler) else: await client.pair() From 88a01551d4d3e4d3e5b3049cdc0979a95101fbfb Mon Sep 17 00:00:00 2001 From: Geoffrey Kruse <6468053+doggkruse@users.noreply.github.com> Date: Tue, 19 Sep 2023 13:39:42 -0700 Subject: [PATCH 03/11] Rename rivian_ble.py to ble.py per feedback on #38 --- src/rivian/{rivian_ble.py => ble.py} | 0 src/rivian/rivian.py | 2 +- 2 files changed, 1 insertion(+), 1 deletion(-) rename src/rivian/{rivian_ble.py => ble.py} (100%) diff --git a/src/rivian/rivian_ble.py b/src/rivian/ble.py similarity index 100% rename from src/rivian/rivian_ble.py rename to src/rivian/ble.py diff --git a/src/rivian/rivian.py b/src/rivian/rivian.py index a1da283..fdea2d3 100644 --- a/src/rivian/rivian.py +++ b/src/rivian/rivian.py @@ -28,7 +28,7 @@ ) from .utils import generate_vehicle_command_hmac from .ws_monitor import WebSocketMonitor -from .rivian_ble import pair_phone +from .ble import pair_phone _LOGGER = logging.getLogger(__name__) From c734ba2e13e85a01d909545ed941869d3825128a Mon Sep 17 00:00:00 2001 From: Geoffrey Kruse <6468053+doggkruse@users.noreply.github.com> Date: Tue, 19 Sep 2023 13:44:24 -0700 Subject: [PATCH 04/11] Address remaining feedback on #38 1) Remove use of global variables 2) Add typing 3) Remove scan_for_device() and require callers to pass in a bleak BLEDevice 4) Open bleak client by device (not by address) 5) Remove retry from pairing function and return status of pairing 6) Check that returned vehicle id matches expected (address todo) --- src/rivian/ble.py | 75 +++++++++++++++++--------------------------- src/rivian/rivian.py | 10 ++++-- 2 files changed, 36 insertions(+), 49 deletions(-) diff --git a/src/rivian/ble.py b/src/rivian/ble.py index 576a96e..0cb1f2e 100644 --- a/src/rivian/ble.py +++ b/src/rivian/ble.py @@ -1,55 +1,35 @@ import platform import asyncio import secrets -from asyncio import TimeoutError -from bleak import BleakScanner, BleakClient, BleakError +import logging + +from bleak import BleakClient, BLEDevice from rivian import utils +_LOGGER = logging.getLogger(__name__) + PHONE_ID_VEHICLE_ID_UUID = "AA49565A-4D4F-424B-4559-5F5752495445" PNONCE_VNONCE_UUID = "E020A15D-E730-4B2C-908B-51DAF9D41E19" BLE_ACTIVE_ENTRY_UUID = "5249565F-4D4F-424B-4559-5F5752495445" -PHONE_KEY_LOCAL_NAME = "Rivian Phone Key" -# Create an asyncio.Event object to signal the arrival of a new notification. -notification_event = asyncio.Event() -notification_data = None - -def notification_handler(sender, data): - global notification_data - notification_data = data - notification_event.set() - -async def scan_for_device(target_device_name): - while True: - # Sleep before the next scan - await asyncio.sleep(1) +async def pair_phone( + device: BLEDevice, vehicle_id, phone_id, vehicle_key, private_key +) -> bool: + success = True - devices = await BleakScanner.discover() - for device in devices: - if device.name is not None and target_device_name in device.name: - print(f"Found Device: {device.name}, Address: {device.address}") - return device.address + # Create an asyncio.Event object to signal the arrival of a new notification. + notification_event = asyncio.Event() + notification_data = None -async def connect_to_device(address): - global client - print(f"Connecting to {address}") + # Callback for notifications from vehicle characteristics + def notification_handler(_, data: bytearray): + nonlocal notification_data + notification_data = data + notification_event.set() try: - client = BleakClient(address, timeout=10.0) - await client.connect() - print(f"Connected: {client.is_connected}") - - return client.is_connected - except (BleakError, TimeoutError, OSError): - print(f"Failed to connect to {address}. Retrying...") - return False - -async def pair_phone(vehicle_id, phone_id, vehicle_key, private_key): - while True: - address = await scan_for_device(PHONE_KEY_LOCAL_NAME) - success = await connect_to_device(address) - - if success: + async with BleakClient(device, timeout=10) as client: + _LOGGER.debug(f"Connecting to {BLEDevice.name} [{BLEDevice.address}]") await client.start_notify(PHONE_ID_VEHICLE_ID_UUID, notification_handler) await client.start_notify(PNONCE_VNONCE_UUID, notification_handler) # wait to enable notifications for BLE_ACTIVE_ENTRY_UUID @@ -59,7 +39,13 @@ async def pair_phone(vehicle_id, phone_id, vehicle_key, private_key): await notification_event.wait() notification_event.clear() - # todo check vehicle id + vas_vehicle_id = notification_data.hex() + if vas_vehicle_id != vehicle_id: + _LOGGER.debug( + "Incorrect vas vehicle id: received %s, expected %s", + vas_vehicle_id, + vehicle_id) + return False # generate pnonce (16-bytes random) pnonce = secrets.token_bytes(16) @@ -76,10 +62,7 @@ async def pair_phone(vehicle_id, phone_id, vehicle_key, private_key): await client.start_notify(BLE_ACTIVE_ENTRY_UUID, notification_handler) else: await client.pair() + except: + success = False - if success: - # todo check other steps above - print("Successfully connected and paired") - break - else: - print("Retrying...") + return success diff --git a/src/rivian/rivian.py b/src/rivian/rivian.py index fdea2d3..c790ec8 100644 --- a/src/rivian/rivian.py +++ b/src/rivian/rivian.py @@ -14,6 +14,8 @@ import async_timeout from aiohttp import ClientResponse, ClientWebSocketResponse +from bleak import BLEDevice + from .const import LIVE_SESSION_PROPERTIES, VEHICLE_STATE_PROPERTIES, VehicleCommand from .exceptions import ( RivianApiException, @@ -280,9 +282,11 @@ async def enroll_phone( return True return False - async def ble_pair_phone(self, vehicle_id, phone_id, vehicle_key, private_key): - """Pair a phone key via BLE.""" - await pair_phone(vehicle_id, phone_id, vehicle_key, private_key) + async def ble_pair_phone( + self, device: BLEDevice, vehicle_id: str, phone_id: str, vehicle_key: str, private_key: str + ) -> bool: + """Connect to and pair a phone key via BLE.""" + await pair_phone(device, vehicle_id, phone_id, vehicle_key, private_key) async def get_drivers_and_keys(self, vehicle_id: str) -> ClientResponse: """Get drivers and keys.""" From bea25d3a40410d2c0c97627ec48d5f2a29eee94f Mon Sep 17 00:00:00 2001 From: Geoffrey Kruse <6468053+doggkruse@users.noreply.github.com> Date: Tue, 19 Sep 2023 13:54:05 -0700 Subject: [PATCH 05/11] Add missing return in shim --- src/rivian/rivian.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/rivian/rivian.py b/src/rivian/rivian.py index c790ec8..c402182 100644 --- a/src/rivian/rivian.py +++ b/src/rivian/rivian.py @@ -286,7 +286,7 @@ async def ble_pair_phone( self, device: BLEDevice, vehicle_id: str, phone_id: str, vehicle_key: str, private_key: str ) -> bool: """Connect to and pair a phone key via BLE.""" - await pair_phone(device, vehicle_id, phone_id, vehicle_key, private_key) + return await pair_phone(device, vehicle_id, phone_id, vehicle_key, private_key) async def get_drivers_and_keys(self, vehicle_id: str) -> ClientResponse: """Get drivers and keys.""" From 40d25992093566c8a307ee9d5bf5474b35be3e96 Mon Sep 17 00:00:00 2001 From: Geoffrey Kruse <6468053+doggkruse@users.noreply.github.com> Date: Tue, 19 Sep 2023 22:20:33 -0700 Subject: [PATCH 06/11] Address additional feedback on #38: 1) general cleanup 2) handle notifications for separately per characteristic to avoid possible mixup 3) add notification timeouts 4) log exceptions --- src/rivian/ble.py | 50 ++++++++++++++++++++++++++++++-------------- src/rivian/rivian.py | 9 -------- 2 files changed, 34 insertions(+), 25 deletions(-) diff --git a/src/rivian/ble.py b/src/rivian/ble.py index 0cb1f2e..7bf8ec3 100644 --- a/src/rivian/ble.py +++ b/src/rivian/ble.py @@ -1,3 +1,5 @@ +"""Rivian BLE handler.""" +from __future__ import annotations import platform import asyncio import secrets @@ -12,32 +14,45 @@ PNONCE_VNONCE_UUID = "E020A15D-E730-4B2C-908B-51DAF9D41E19" BLE_ACTIVE_ENTRY_UUID = "5249565F-4D4F-424B-4559-5F5752495445" +NOTIFICATION_TIMEOUT = 3.0 +CONNECT_TIMEOUT = 10.0 + async def pair_phone( - device: BLEDevice, vehicle_id, phone_id, vehicle_key, private_key + device: BLEDevice, vehicle_id: str, phone_id: str, vehicle_key: str, private_key: str ) -> bool: - success = True + success = False # Create an asyncio.Event object to signal the arrival of a new notification. - notification_event = asyncio.Event() - notification_data = None + vid_event = asyncio.Event() + nonce_event = asyncio.Event() + notification_data: bytearray | None = None # Callback for notifications from vehicle characteristics - def notification_handler(_, data: bytearray): + def id_notification_handler(_, data: bytearray) -> None: + nonlocal notification_data + notification_data = data + vid_event.set() + + def nonce_notification_handler(_, data: bytearray) -> None: nonlocal notification_data notification_data = data - notification_event.set() + nonce_event.set() + + # this is a dummy callback (unused) + def active_entry_notification_handler(_, data: bytearray) -> None: + pass try: - async with BleakClient(device, timeout=10) as client: - _LOGGER.debug(f"Connecting to {BLEDevice.name} [{BLEDevice.address}]") - await client.start_notify(PHONE_ID_VEHICLE_ID_UUID, notification_handler) - await client.start_notify(PNONCE_VNONCE_UUID, notification_handler) + _LOGGER.debug(f"Connecting to {BLEDevice.name} [{BLEDevice.address}]") + async with BleakClient(device, timeout=CONNECT_TIMEOUT) as client: + _LOGGER.debug(f"Connected to {BLEDevice.name} [{BLEDevice.address}]") + await client.start_notify(PHONE_ID_VEHICLE_ID_UUID, id_notification_handler) + await client.start_notify(PNONCE_VNONCE_UUID, nonce_notification_handler) # wait to enable notifications for BLE_ACTIVE_ENTRY_UUID # write the phone ID (16-bytes) response will be vehicle ID await client.write_gatt_char(PHONE_ID_VEHICLE_ID_UUID, bytes.fromhex(phone_id.replace("-", ""))) - await notification_event.wait() - notification_event.clear() + await asyncio.wait_for(vid_event.wait(), NOTIFICATION_TIMEOUT) vas_vehicle_id = notification_data.hex() if vas_vehicle_id != vehicle_id: @@ -53,16 +68,19 @@ def notification_handler(_, data: bytearray): # write pnonce (48-bytes) response will be vnonce await client.write_gatt_char(PNONCE_VNONCE_UUID, pnonce + hmac ) - await notification_event.wait() + await asyncio.wait_for(nonce_event.wait(), NOTIFICATION_TIMEOUT) # vehicle is authenticated, trigger bonding if platform.system() == "Darwin": # Mac BLE API doesn't have an explicit way to trigger bonding # enable notification on BLE_ACTIVE_ENTRY_UUID to trigger bonding - await client.start_notify(BLE_ACTIVE_ENTRY_UUID, notification_handler) + await client.start_notify(BLE_ACTIVE_ENTRY_UUID, active_entry_notification_handler) else: await client.pair() - except: - success = False + + success = True + + except Exception as e: + _LOGGER.debug(f"An exception occurred while pairing: {str(e)}") return success diff --git a/src/rivian/rivian.py b/src/rivian/rivian.py index c402182..0ebf3ff 100644 --- a/src/rivian/rivian.py +++ b/src/rivian/rivian.py @@ -14,8 +14,6 @@ import async_timeout from aiohttp import ClientResponse, ClientWebSocketResponse -from bleak import BLEDevice - from .const import LIVE_SESSION_PROPERTIES, VEHICLE_STATE_PROPERTIES, VehicleCommand from .exceptions import ( RivianApiException, @@ -30,7 +28,6 @@ ) from .utils import generate_vehicle_command_hmac from .ws_monitor import WebSocketMonitor -from .ble import pair_phone _LOGGER = logging.getLogger(__name__) @@ -281,12 +278,6 @@ async def enroll_phone( if data.get("data", {}).get("enrollPhone", {}).get("success"): return True return False - - async def ble_pair_phone( - self, device: BLEDevice, vehicle_id: str, phone_id: str, vehicle_key: str, private_key: str - ) -> bool: - """Connect to and pair a phone key via BLE.""" - return await pair_phone(device, vehicle_id, phone_id, vehicle_key, private_key) async def get_drivers_and_keys(self, vehicle_id: str) -> ClientResponse: """Get drivers and keys.""" From 8132aeb92946223e0df973fd6756c7b877098ba7 Mon Sep 17 00:00:00 2001 From: Geoffrey Kruse <6468053+doggkruse@users.noreply.github.com> Date: Thu, 21 Sep 2023 21:24:09 -0700 Subject: [PATCH 07/11] strip "-" when comparing vehicle ID --- src/rivian/ble.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/rivian/ble.py b/src/rivian/ble.py index 7bf8ec3..3937172 100644 --- a/src/rivian/ble.py +++ b/src/rivian/ble.py @@ -55,7 +55,7 @@ def active_entry_notification_handler(_, data: bytearray) -> None: await asyncio.wait_for(vid_event.wait(), NOTIFICATION_TIMEOUT) vas_vehicle_id = notification_data.hex() - if vas_vehicle_id != vehicle_id: + if vas_vehicle_id != vehicle_id.replace("-", ""): _LOGGER.debug( "Incorrect vas vehicle id: received %s, expected %s", vas_vehicle_id, From 7cdef87b4309393f0f725be601e67a5900f7a135 Mon Sep 17 00:00:00 2001 From: Geoffrey Kruse <6468053+doggkruse@users.noreply.github.com> Date: Sat, 7 Oct 2023 13:05:13 -0700 Subject: [PATCH 08/11] Use %s for logging string formatting per PR feedback --- src/rivian/ble.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/rivian/ble.py b/src/rivian/ble.py index 3937172..08a411f 100644 --- a/src/rivian/ble.py +++ b/src/rivian/ble.py @@ -43,9 +43,9 @@ def active_entry_notification_handler(_, data: bytearray) -> None: pass try: - _LOGGER.debug(f"Connecting to {BLEDevice.name} [{BLEDevice.address}]") + _LOGGER.debug("Connecting to %s [%s]", BLEDevice.name, BLEDevice.address) async with BleakClient(device, timeout=CONNECT_TIMEOUT) as client: - _LOGGER.debug(f"Connected to {BLEDevice.name} [{BLEDevice.address}]") + _LOGGER.debug("Connected to %s [%s]", BLEDevice.name, BLEDevice.address) await client.start_notify(PHONE_ID_VEHICLE_ID_UUID, id_notification_handler) await client.start_notify(PNONCE_VNONCE_UUID, nonce_notification_handler) # wait to enable notifications for BLE_ACTIVE_ENTRY_UUID @@ -81,6 +81,6 @@ def active_entry_notification_handler(_, data: bytearray) -> None: success = True except Exception as e: - _LOGGER.debug(f"An exception occurred while pairing: {str(e)}") + _LOGGER.debug("An exception occurred while pairing: %s", str(e)) return success From 33771bbd0051895c586d8541f39b2e2efa02c77c Mon Sep 17 00:00:00 2001 From: Nathan Spencer Date: Sat, 11 Nov 2023 16:26:35 -0700 Subject: [PATCH 09/11] Make bleak optional and add some helper functions for ble pairing --- poetry.lock | 402 ++++++++++++++++++++++++++++++++++++++++++- pyproject.toml | 6 +- src/rivian/ble.py | 196 +++++++++++++++------ src/rivian/rivian.py | 4 +- src/rivian/utils.py | 10 +- 5 files changed, 552 insertions(+), 66 deletions(-) diff --git a/poetry.lock b/poetry.lock index 0d200b7..bc1e0ff 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,4 +1,4 @@ -# This file is automatically @generated by Poetry 1.5.1 and should not be changed by hand. +# This file is automatically @generated by Poetry 1.6.1 and should not be changed by hand. [[package]] name = "aiohttp" @@ -176,6 +176,53 @@ files = [ {file = "backports.strenum-1.2.4.tar.gz", hash = "sha256:87b67fd1413af3ce959b565d84ddef99776cdd97a62e2fd2d0550cdacc210dee"}, ] +[[package]] +name = "bleak" +version = "0.21.1" +description = "Bluetooth Low Energy platform Agnostic Klient" +optional = true +python-versions = ">=3.8,<3.13" +files = [ + {file = "bleak-0.21.1-py3-none-any.whl", hash = "sha256:ccec260a0f5ec02dd133d68b0351c0151b2ecf3ddd0bcabc4c04a1cdd7f33256"}, + {file = "bleak-0.21.1.tar.gz", hash = "sha256:ec4a1a2772fb315b992cbaa1153070c7e26968a52b0e2727035f443a1af5c18f"}, +] + +[package.dependencies] +async-timeout = {version = ">=3.0.0,<5", markers = "python_version < \"3.11\""} +bleak-winrt = {version = ">=1.2.0,<2.0.0", markers = "platform_system == \"Windows\" and python_version < \"3.12\""} +dbus-fast = {version = ">=1.83.0,<3", markers = "platform_system == \"Linux\""} +pyobjc-core = {version = ">=9.2,<10.0", markers = "platform_system == \"Darwin\""} +pyobjc-framework-CoreBluetooth = {version = ">=9.2,<10.0", markers = "platform_system == \"Darwin\""} +pyobjc-framework-libdispatch = {version = ">=9.2,<10.0", markers = "platform_system == \"Darwin\""} +typing-extensions = {version = ">=4.7.0", markers = "python_version < \"3.12\""} +"winrt-Windows.Devices.Bluetooth" = {version = "2.0.0b1", markers = "platform_system == \"Windows\" and python_version >= \"3.12\""} +"winrt-Windows.Devices.Bluetooth.Advertisement" = {version = "2.0.0b1", markers = "platform_system == \"Windows\" and python_version >= \"3.12\""} +"winrt-Windows.Devices.Bluetooth.GenericAttributeProfile" = {version = "2.0.0b1", markers = "platform_system == \"Windows\" and python_version >= \"3.12\""} +"winrt-Windows.Devices.Enumeration" = {version = "2.0.0b1", markers = "platform_system == \"Windows\" and python_version >= \"3.12\""} +"winrt-Windows.Foundation" = {version = "2.0.0b1", markers = "platform_system == \"Windows\" and python_version >= \"3.12\""} +"winrt-Windows.Foundation.Collections" = {version = "2.0.0b1", markers = "platform_system == \"Windows\" and python_version >= \"3.12\""} +"winrt-Windows.Storage.Streams" = {version = "2.0.0b1", markers = "platform_system == \"Windows\" and python_version >= \"3.12\""} + +[[package]] +name = "bleak-winrt" +version = "1.2.0" +description = "Python WinRT bindings for Bleak" +optional = true +python-versions = "*" +files = [ + {file = "bleak-winrt-1.2.0.tar.gz", hash = "sha256:0577d070251b9354fc6c45ffac57e39341ebb08ead014b1bdbd43e211d2ce1d6"}, + {file = "bleak_winrt-1.2.0-cp310-cp310-win32.whl", hash = "sha256:a2ae3054d6843ae0cfd3b94c83293a1dfd5804393977dd69bde91cb5099fc47c"}, + {file = "bleak_winrt-1.2.0-cp310-cp310-win_amd64.whl", hash = "sha256:677df51dc825c6657b3ae94f00bd09b8ab88422b40d6a7bdbf7972a63bc44e9a"}, + {file = "bleak_winrt-1.2.0-cp311-cp311-win32.whl", hash = "sha256:9449cdb942f22c9892bc1ada99e2ccce9bea8a8af1493e81fefb6de2cb3a7b80"}, + {file = "bleak_winrt-1.2.0-cp311-cp311-win_amd64.whl", hash = "sha256:98c1b5a6a6c431ac7f76aa4285b752fe14a1c626bd8a1dfa56f66173ff120bee"}, + {file = "bleak_winrt-1.2.0-cp37-cp37m-win32.whl", hash = "sha256:623ac511696e1f58d83cb9c431e32f613395f2199b3db7f125a3d872cab968a4"}, + {file = "bleak_winrt-1.2.0-cp37-cp37m-win_amd64.whl", hash = "sha256:13ab06dec55469cf51a2c187be7b630a7a2922e1ea9ac1998135974a7239b1e3"}, + {file = "bleak_winrt-1.2.0-cp38-cp38-win32.whl", hash = "sha256:5a36ff8cd53068c01a795a75d2c13054ddc5f99ce6de62c1a97cd343fc4d0727"}, + {file = "bleak_winrt-1.2.0-cp38-cp38-win_amd64.whl", hash = "sha256:810c00726653a962256b7acd8edf81ab9e4a3c66e936a342ce4aec7dbd3a7263"}, + {file = "bleak_winrt-1.2.0-cp39-cp39-win32.whl", hash = "sha256:dd740047a08925bde54bec357391fcee595d7b8ca0c74c87170a5cbc3f97aa0a"}, + {file = "bleak_winrt-1.2.0-cp39-cp39-win_amd64.whl", hash = "sha256:63130c11acfe75c504a79c01f9919e87f009f5e742bfc7b7a5c2a9c72bf591a7"}, +] + [[package]] name = "cffi" version = "1.15.1" @@ -388,6 +435,49 @@ ssh = ["bcrypt (>=3.1.5)"] test = ["pretend", "pytest (>=6.2.0)", "pytest-benchmark", "pytest-cov", "pytest-xdist"] test-randomorder = ["pytest-randomly"] +[[package]] +name = "dbus-fast" +version = "2.14.0" +description = "A faster version of dbus-next" +optional = true +python-versions = ">=3.7,<4.0" +files = [ + {file = "dbus_fast-2.14.0-cp310-cp310-manylinux_2_17_i686.manylinux_2_5_i686.manylinux1_i686.manylinux2014_i686.whl", hash = "sha256:5c6cf49d8e468edeadb650a0f0484d5ba89b114fed28c790fd20180051c3d307"}, + {file = "dbus_fast-2.14.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:40a8bbbc825d5bd85cdc761fa3c26bc1d5a3d0df54fc04b173d461081149ecf4"}, + {file = "dbus_fast-2.14.0-cp310-cp310-manylinux_2_31_x86_64.whl", hash = "sha256:2c11b254a4eb1753b69343f025ede9bb6e089d1e7d03d3bf304f62f3e07ff32c"}, + {file = "dbus_fast-2.14.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:e9a883c11ccd0eea04adf20ac02ba7b1c9e2fd1b0164696b5ad439427fff4e74"}, + {file = "dbus_fast-2.14.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:eb0cc551df7267239f670110a7a117568a5aab5eac9c55ee0a6f43ec22836def"}, + {file = "dbus_fast-2.14.0-cp311-cp311-manylinux_2_17_i686.manylinux_2_5_i686.manylinux1_i686.manylinux2014_i686.whl", hash = "sha256:8fba4167d60f91b54ea663ee02cdf33fed31017d351bd1597296c5aee3a3a4ef"}, + {file = "dbus_fast-2.14.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:daeba10cc67a645554dc4feaff913a4ec895a86b1ca0ea9e53f2a1af9b9beb2e"}, + {file = "dbus_fast-2.14.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:087a4786b961a87156780e2d4ba40365828fe0503bac9d91d4b757f448aa13b0"}, + {file = "dbus_fast-2.14.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:94b1e36adfca296d8e916d299a44846de76c5d025f48c7958eed5b902faa625e"}, + {file = "dbus_fast-2.14.0-cp312-cp312-manylinux_2_17_i686.manylinux_2_5_i686.manylinux1_i686.manylinux2014_i686.whl", hash = "sha256:8669a6e834291ca4c236f9e7fd649d8dfc48113224b7f61876de4b97da754daf"}, + {file = "dbus_fast-2.14.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d6388fc28577020afa3a2595f56c27266a59f90471085d93d60def889ff1c0cd"}, + {file = "dbus_fast-2.14.0-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:8f4ba9baf4f36b22ce5968963277b97c0e39277eeecc6fb90fe8acb15cecf321"}, + {file = "dbus_fast-2.14.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:076cfcf61be10ddc09ceba15d96efe678ccaa9f818615d30b46bc3cdfa668beb"}, + {file = "dbus_fast-2.14.0-cp37-cp37m-manylinux_2_17_i686.manylinux_2_5_i686.manylinux1_i686.manylinux2014_i686.whl", hash = "sha256:9efb885bcda111c505777a7303c048e7c0d95db3145058b89eff8269a43beeea"}, + {file = "dbus_fast-2.14.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a6610ce49fc13af1d3cf619bd858d498624846a25f1a256ef68ec9f586786ff0"}, + {file = "dbus_fast-2.14.0-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:791b445164568f9d081b537f3b61053caf9e6ec487807355707c804301ab369f"}, + {file = "dbus_fast-2.14.0-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:fcfe991ab96c603bf1d9a63eefa29254e32eba621eaa56d9562f89d62e86cf83"}, + {file = "dbus_fast-2.14.0-cp38-cp38-manylinux_2_17_i686.manylinux_2_5_i686.manylinux1_i686.manylinux2014_i686.whl", hash = "sha256:b732f77b444c1bcf74c936bd2a0998c1f4ad69d67a83378307c975e052f73b28"}, + {file = "dbus_fast-2.14.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:62c0f01b9d49d831d54dcd6325bb196773ffe70d64daf267fe518add4c5a0adf"}, + {file = "dbus_fast-2.14.0-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:54fd558eda0093d68d5e84f2f5bcc7f1e0cf9fb40b3b301241f0f31a5af78750"}, + {file = "dbus_fast-2.14.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:0573f0cf750f5fe28e9a1cde979c493dd8407526860f3344144790d07a9ff89a"}, + {file = "dbus_fast-2.14.0-cp39-cp39-manylinux_2_17_i686.manylinux_2_5_i686.manylinux1_i686.manylinux2014_i686.whl", hash = "sha256:3cf8e9f21edc9d45e86e9bd431081f84996d23740478b10e63770d8739d2b371"}, + {file = "dbus_fast-2.14.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:db69da2592b28400caaf1d418623e6f3406466e6b2e659c0df3db59635ea13b6"}, + {file = "dbus_fast-2.14.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:8dfda5efc774ee4fdf3bedf2fb695df23901ceb29b0429d27e76814af64bee20"}, + {file = "dbus_fast-2.14.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:2a96fb212c4641b603e2ec27e687f90cec636082b3e9998f804bd749006f117e"}, + {file = "dbus_fast-2.14.0-pp310-pypy310_pp73-manylinux_2_17_i686.manylinux_2_5_i686.manylinux1_i686.manylinux2014_i686.whl", hash = "sha256:57a12d6333a4ea3ef2d179aca115c7c003384b7b91fa7851f02b157524ba88e9"}, + {file = "dbus_fast-2.14.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux_2_5_x86_64.manylinux1_x86_64.manylinux2014_x86_64.whl", hash = "sha256:05be1fb02e61c9a549f274a79d5c4e58478e16e8d5cba104f3005f3c404cba41"}, + {file = "dbus_fast-2.14.0-pp37-pypy37_pp73-manylinux_2_17_i686.manylinux_2_5_i686.manylinux1_i686.manylinux2014_i686.whl", hash = "sha256:261c7374ed17bbdaa9e239da3d7c1ba842df7f665114a6bd766f42e4d246efdf"}, + {file = "dbus_fast-2.14.0-pp37-pypy37_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:197fec95091c106ba975d47b820e3ed58fd489f1370d7d91e4c32766adb4be09"}, + {file = "dbus_fast-2.14.0-pp38-pypy38_pp73-manylinux_2_17_i686.manylinux_2_5_i686.manylinux1_i686.manylinux2014_i686.whl", hash = "sha256:a0d084db57286c8cf60caa36ad4ba82dd00d17f664951e680d151832eb4fa483"}, + {file = "dbus_fast-2.14.0-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux_2_5_x86_64.manylinux1_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1f02e7b174b0c366ac114dc056f998825e72cf8905c0e6ce3deea535af2becf4"}, + {file = "dbus_fast-2.14.0-pp39-pypy39_pp73-manylinux_2_17_i686.manylinux_2_5_i686.manylinux1_i686.manylinux2014_i686.whl", hash = "sha256:28520febacec285e2a0a607a01751f82b89791dcf544b8a7df34b5a0a6e3f6bd"}, + {file = "dbus_fast-2.14.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux_2_5_x86_64.manylinux1_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2f1dc8a0ad1a3f9957a7bb566be7c79ab87b52c3d6e9f642bfa31290abfda4c1"}, + {file = "dbus_fast-2.14.0.tar.gz", hash = "sha256:91a88fea66b4e69ce73ac4c1ac04952c4fbd5e9b902400649013778a177129ea"}, +] + [[package]] name = "exceptiongroup" version = "1.1.2" @@ -614,6 +704,80 @@ files = [ {file = "pycparser-2.21.tar.gz", hash = "sha256:e644fdec12f7872f86c58ff790da456218b10f863970249516d60a5eaca77206"}, ] +[[package]] +name = "pyobjc-core" +version = "9.2" +description = "Python<->ObjC Interoperability Module" +optional = true +python-versions = ">=3.7" +files = [ + {file = "pyobjc-core-9.2.tar.gz", hash = "sha256:d734b9291fec91ff4e3ae38b9c6839debf02b79c07314476e87da8e90b2c68c3"}, + {file = "pyobjc_core-9.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:fa674a39949f5cde8e5c7bbcd24496446bfc67592b028aedbec7f81dc5fc4daa"}, + {file = "pyobjc_core-9.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:bbc8de304ee322a1ee530b4d2daca135a49b4a49aa3cedc6b2c26c43885f4842"}, + {file = "pyobjc_core-9.2-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:0fa950f092673883b8bd28bc18397415cabb457bf410920762109b411789ade9"}, + {file = "pyobjc_core-9.2-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:586e4cae966282eaa61b21cae66ccdcee9d69c036979def26eebdc08ddebe20f"}, + {file = "pyobjc_core-9.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:41189c2c680931c0395a55691763c481fc681f454f21bb4f1644f98c24a45954"}, + {file = "pyobjc_core-9.2-cp38-cp38-macosx_11_0_universal2.whl", hash = "sha256:2d23ee539f2ba5e9f5653d75a13f575c7e36586fc0086792739e69e4c2617eda"}, + {file = "pyobjc_core-9.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:b9809cf96678797acb72a758f34932fe8e2602d5ab7abec15c5ac68ddb481720"}, +] + +[[package]] +name = "pyobjc-framework-cocoa" +version = "9.2" +description = "Wrappers for the Cocoa frameworks on macOS" +optional = true +python-versions = ">=3.7" +files = [ + {file = "pyobjc-framework-Cocoa-9.2.tar.gz", hash = "sha256:efd78080872d8c8de6c2b97e0e4eac99d6203a5d1637aa135d071d464eb2db53"}, + {file = "pyobjc_framework_Cocoa-9.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:9e02d8a7cc4eb7685377c50ba4f17345701acf4c05b1e7480d421bff9e2f62a4"}, + {file = "pyobjc_framework_Cocoa-9.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:3b1e6287b3149e4c6679cdbccd8e9ef6557a4e492a892e80a77df143f40026d2"}, + {file = "pyobjc_framework_Cocoa-9.2-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:312977ce2e3989073c6b324c69ba24283de206fe7acd6dbbbaf3e29238a22537"}, + {file = "pyobjc_framework_Cocoa-9.2-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:aae7841cf40c26dd915f4dd828f91c6616e6b7998630b72e704750c09e00f334"}, + {file = "pyobjc_framework_Cocoa-9.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:739a421e14382a46cbeb9a883f192dceff368ad28ec34d895c48c0ad34cf2c1d"}, + {file = "pyobjc_framework_Cocoa-9.2-cp38-cp38-macosx_11_0_universal2.whl", hash = "sha256:32d9ac1033fac1b821ddee8c68f972a7074ad8c50bec0bea9a719034c1c2fb94"}, + {file = "pyobjc_framework_Cocoa-9.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:b236bb965e41aeb2e215d4e98a5a230d4b63252c6d26e00924ea2e69540a59d6"}, +] + +[package.dependencies] +pyobjc-core = ">=9.2" + +[[package]] +name = "pyobjc-framework-corebluetooth" +version = "9.2" +description = "Wrappers for the framework CoreBluetooth on macOS" +optional = true +python-versions = ">=3.7" +files = [ + {file = "pyobjc-framework-CoreBluetooth-9.2.tar.gz", hash = "sha256:cb2481b1dfe211ae9ce55f36537dc8155dbf0dc8ff26e0bc2e13f7afb0a291d1"}, + {file = "pyobjc_framework_CoreBluetooth-9.2-cp36-abi3-macosx_10_9_universal2.whl", hash = "sha256:53d888742119d0f0c725d0b0c2389f68e8f21f0cba6d6aec288c53260a0196b6"}, + {file = "pyobjc_framework_CoreBluetooth-9.2-cp36-abi3-macosx_10_9_x86_64.whl", hash = "sha256:179532882126526e38fe716a50fb0ee8f440e0b838d290252c515e622b5d0e49"}, + {file = "pyobjc_framework_CoreBluetooth-9.2-cp36-abi3-macosx_11_0_universal2.whl", hash = "sha256:256a5031ea9d8a7406541fa1b0dfac549b1de93deae8284605f9355b13fb58be"}, +] + +[package.dependencies] +pyobjc-core = ">=9.2" +pyobjc-framework-Cocoa = ">=9.2" + +[[package]] +name = "pyobjc-framework-libdispatch" +version = "9.2" +description = "Wrappers for libdispatch on macOS" +optional = true +python-versions = ">=3.7" +files = [ + {file = "pyobjc-framework-libdispatch-9.2.tar.gz", hash = "sha256:542e7f7c2b041939db5ed6f3119c1d67d73ec14a996278b92485f8513039c168"}, + {file = "pyobjc_framework_libdispatch-9.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:88d4091d4bcb5702783d6e86b4107db973425a17d1de491543f56bd348909b60"}, + {file = "pyobjc_framework_libdispatch-9.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:1a67b007113328538b57893cc7829a722270764cdbeae6d5e1460a1d911314df"}, + {file = "pyobjc_framework_libdispatch-9.2-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:6fccea1a57436cf1ac50d9ebc6e3e725bcf77f829ba6b118e62e6ed7866d359d"}, + {file = "pyobjc_framework_libdispatch-9.2-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:6eba747b7ad91b0463265a7aee59235bb051fb97687f35ca2233690369b5e4e4"}, + {file = "pyobjc_framework_libdispatch-9.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:2e835495860d04f63c2d2f73ae3dd79da4222864c107096dc0f99e8382700026"}, + {file = "pyobjc_framework_libdispatch-9.2-cp38-cp38-macosx_11_0_universal2.whl", hash = "sha256:1b107e5c3580b09553030961ea6b17abad4a5132101eab1af3ad2cb36d0f08bb"}, + {file = "pyobjc_framework_libdispatch-9.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:83cdb672acf722717b5ecf004768f215f02ac02d7f7f2a9703da6e921ab02222"}, +] + +[package.dependencies] +pyobjc-core = ">=9.2" + [[package]] name = "pytest" version = "7.4.0" @@ -679,6 +843,235 @@ files = [ {file = "tomli-2.0.1.tar.gz", hash = "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f"}, ] +[[package]] +name = "typing-extensions" +version = "4.8.0" +description = "Backported and Experimental Type Hints for Python 3.8+" +optional = true +python-versions = ">=3.8" +files = [ + {file = "typing_extensions-4.8.0-py3-none-any.whl", hash = "sha256:8f92fc8806f9a6b641eaa5318da32b44d401efaac0f6678c9bc448ba3605faa0"}, + {file = "typing_extensions-4.8.0.tar.gz", hash = "sha256:df8e4339e9cb77357558cbdbceca33c303714cf861d1eef15e1070055ae8b7ef"}, +] + +[[package]] +name = "winrt-runtime" +version = "2.0.0b1" +description = "Python projection of Windows Runtime (WinRT) APIs" +optional = true +python-versions = "<3.13,>=3.9" +files = [ + {file = "winrt-runtime-2.0.0b1.tar.gz", hash = "sha256:28db2ebe7bfb347d110224e9f23fe8079cea45af0fcbd643d039524ced07d22c"}, + {file = "winrt_runtime-2.0.0b1-cp310-cp310-win32.whl", hash = "sha256:8f812b01e2c8dd3ca68aa51a7aa02e815cc2ac3c8520a883b4ec7a4fc63afb04"}, + {file = "winrt_runtime-2.0.0b1-cp310-cp310-win_amd64.whl", hash = "sha256:f36f6102f9b7a08d917a6809117c085639b66be2c579f4089d3fd47b83e8f87b"}, + {file = "winrt_runtime-2.0.0b1-cp310-cp310-win_arm64.whl", hash = "sha256:4a99f267da96edc977623355b816b46c1344c66dc34732857084417d8cf9a96b"}, + {file = "winrt_runtime-2.0.0b1-cp311-cp311-win32.whl", hash = "sha256:ba998e3fc452338c5e2d7bf5174a6206580245066d60079ee4130082d0eb61c2"}, + {file = "winrt_runtime-2.0.0b1-cp311-cp311-win_amd64.whl", hash = "sha256:e7838f0fdf5653ce245888590214177a1f54884cece2c8dfbfe3d01b2780171e"}, + {file = "winrt_runtime-2.0.0b1-cp311-cp311-win_arm64.whl", hash = "sha256:2afa45b7385e99a63d55ccda29096e6a84fcd4c654479005c147b0e65e274abf"}, + {file = "winrt_runtime-2.0.0b1-cp312-cp312-win32.whl", hash = "sha256:edda124ff965cec3a6bfdb26fbe88e004f96975dd84115176e30c1efbcb16f4c"}, + {file = "winrt_runtime-2.0.0b1-cp312-cp312-win_amd64.whl", hash = "sha256:d8935951efeec6b3d546dce8f48bb203aface57a1ba991c066f0e12e84c8f91e"}, + {file = "winrt_runtime-2.0.0b1-cp312-cp312-win_arm64.whl", hash = "sha256:509fb9a03af5e1125433f58522725716ceef040050d33625460b5a5eb98a46ac"}, + {file = "winrt_runtime-2.0.0b1-cp39-cp39-win32.whl", hash = "sha256:41138fe4642345d7143e817ce0905d82e60b3832558143e0a17bfea8654c6512"}, + {file = "winrt_runtime-2.0.0b1-cp39-cp39-win_amd64.whl", hash = "sha256:081a429fe85c33cb6610c4a799184b7650b30f15ab1d89866f2bda246d3a5c0a"}, + {file = "winrt_runtime-2.0.0b1-cp39-cp39-win_arm64.whl", hash = "sha256:e6984604c6ae1f3258973ba2503d1ea5aa15e536ca41d6a131ad305ebbb6519d"}, +] + +[[package]] +name = "winrt-windows-devices-bluetooth" +version = "2.0.0b1" +description = "Python projection of Windows Runtime (WinRT) APIs" +optional = true +python-versions = "<3.13,>=3.9" +files = [ + {file = "winrt-Windows.Devices.Bluetooth-2.0.0b1.tar.gz", hash = "sha256:786bd43786b873a083b89debece538974f720584662a2573d6a8a8501a532860"}, + {file = "winrt_Windows.Devices.Bluetooth-2.0.0b1-cp310-cp310-win32.whl", hash = "sha256:79631bf3f96954da260859df9228a028835ffade0d885ba3942c5a86a853d150"}, + {file = "winrt_Windows.Devices.Bluetooth-2.0.0b1-cp310-cp310-win_amd64.whl", hash = "sha256:cd85337a95065d0d2045c06db1a5edd4a447aad47cf7027818f6fb69f831c56c"}, + {file = "winrt_Windows.Devices.Bluetooth-2.0.0b1-cp310-cp310-win_arm64.whl", hash = "sha256:6a963869ed003d260e90e9bedc334129303f263f068ea1c0d994df53317db2bc"}, + {file = "winrt_Windows.Devices.Bluetooth-2.0.0b1-cp311-cp311-win32.whl", hash = "sha256:7c5951943a3911d94a8da190f4355dc70128d7d7f696209316372c834b34d462"}, + {file = "winrt_Windows.Devices.Bluetooth-2.0.0b1-cp311-cp311-win_amd64.whl", hash = "sha256:b0bb154ae92235649ed234982f609c490a467d5049c27d63397be9abbb00730e"}, + {file = "winrt_Windows.Devices.Bluetooth-2.0.0b1-cp311-cp311-win_arm64.whl", hash = "sha256:6688dfb0fc3b7dc517bf8cf40ae00544a50b4dec91470d37be38fc33c4523632"}, + {file = "winrt_Windows.Devices.Bluetooth-2.0.0b1-cp312-cp312-win32.whl", hash = "sha256:613c6ff4125df46189b3bef6d3110d94ec725d357ab734f00eedb11c4116c367"}, + {file = "winrt_Windows.Devices.Bluetooth-2.0.0b1-cp312-cp312-win_amd64.whl", hash = "sha256:59c403b64e9f4e417599c6f6aea6ee6fac960597c21eac6b3fd8a84f64aa387c"}, + {file = "winrt_Windows.Devices.Bluetooth-2.0.0b1-cp312-cp312-win_arm64.whl", hash = "sha256:b7f6e1b9bb6e33be80045adebd252cf25cd648759fad6e86c61a393ddd709f7f"}, + {file = "winrt_Windows.Devices.Bluetooth-2.0.0b1-cp39-cp39-win32.whl", hash = "sha256:eae7a89106eab047e96843e28c3c6ce0886dd7dee60180a1010498925e9503f9"}, + {file = "winrt_Windows.Devices.Bluetooth-2.0.0b1-cp39-cp39-win_amd64.whl", hash = "sha256:8dfd1915c894ac19dd0b24aba38ef676c92c3473c0d9826762ba9616ad7df68b"}, + {file = "winrt_Windows.Devices.Bluetooth-2.0.0b1-cp39-cp39-win_arm64.whl", hash = "sha256:49058587e6d82ba33da0767b97a378ddfea8e3a5991bdeff680faa287bfae57e"}, +] + +[package.dependencies] +winrt-runtime = "2.0.0-beta.1" + +[package.extras] +all = ["winrt-Windows.Devices.Bluetooth.GenericAttributeProfile[all] (==2.0.0-beta.1)", "winrt-Windows.Devices.Bluetooth.Rfcomm[all] (==2.0.0-beta.1)", "winrt-Windows.Devices.Enumeration[all] (==2.0.0-beta.1)", "winrt-Windows.Devices.Radios[all] (==2.0.0-beta.1)", "winrt-Windows.Foundation.Collections[all] (==2.0.0-beta.1)", "winrt-Windows.Foundation[all] (==2.0.0-beta.1)", "winrt-Windows.Networking[all] (==2.0.0-beta.1)", "winrt-Windows.Storage.Streams[all] (==2.0.0-beta.1)"] + +[[package]] +name = "winrt-windows-devices-bluetooth-advertisement" +version = "2.0.0b1" +description = "Python projection of Windows Runtime (WinRT) APIs" +optional = true +python-versions = "<3.13,>=3.9" +files = [ + {file = "winrt-Windows.Devices.Bluetooth.Advertisement-2.0.0b1.tar.gz", hash = "sha256:d9050faa4377d410d4f0e9cabb5ec555a267531c9747370555ac9ec93ec9f399"}, + {file = "winrt_Windows.Devices.Bluetooth.Advertisement-2.0.0b1-cp310-cp310-win32.whl", hash = "sha256:ac9b703d16adc87c3541585525b8fcf6d84391e2fa010c2f001e714c405cc3b7"}, + {file = "winrt_Windows.Devices.Bluetooth.Advertisement-2.0.0b1-cp310-cp310-win_amd64.whl", hash = "sha256:593cade7853a8b0770e8ef30462b5d5f477b82e17e0aa590094b1c26efd3e05a"}, + {file = "winrt_Windows.Devices.Bluetooth.Advertisement-2.0.0b1-cp310-cp310-win_arm64.whl", hash = "sha256:574698c08895e2cfee7379bdf34a5f319fe440d7dfcc7bc9858f457c08e9712c"}, + {file = "winrt_Windows.Devices.Bluetooth.Advertisement-2.0.0b1-cp311-cp311-win32.whl", hash = "sha256:652a096f8210036bbb539d7f971eaf1f472a3aeb60b7e31278e3d0d30a355292"}, + {file = "winrt_Windows.Devices.Bluetooth.Advertisement-2.0.0b1-cp311-cp311-win_amd64.whl", hash = "sha256:e5cfb866c44dad644fb44b441f4fdbddafc9564075f1f68f756e20f438105c67"}, + {file = "winrt_Windows.Devices.Bluetooth.Advertisement-2.0.0b1-cp311-cp311-win_arm64.whl", hash = "sha256:6c2503eaaf5cd988b5510b86347dba45ad6ee52656f9656a1a97abae6d35386e"}, + {file = "winrt_Windows.Devices.Bluetooth.Advertisement-2.0.0b1-cp312-cp312-win32.whl", hash = "sha256:780c766725a55f4211f921c773c92c2331803e70f65d6ad6676a60f903d39a54"}, + {file = "winrt_Windows.Devices.Bluetooth.Advertisement-2.0.0b1-cp312-cp312-win_amd64.whl", hash = "sha256:39c8633d01039eb2c2f6f20cfc43c045a333b9f3a45229e2ce443f71bb2a562c"}, + {file = "winrt_Windows.Devices.Bluetooth.Advertisement-2.0.0b1-cp312-cp312-win_arm64.whl", hash = "sha256:eaa0d44b4158b16937eac8102249e792f0299dbb0aefc56cc9adc9552e8f9afe"}, + {file = "winrt_Windows.Devices.Bluetooth.Advertisement-2.0.0b1-cp39-cp39-win32.whl", hash = "sha256:d171487e23f7671ad2923544bfa6545d0a29a1a9ae1f5c1d5e5e5f473a5d62b2"}, + {file = "winrt_Windows.Devices.Bluetooth.Advertisement-2.0.0b1-cp39-cp39-win_amd64.whl", hash = "sha256:442eecac87653a03617e65bdb2ef79ddc0582dfdacc2be8af841fba541577f8b"}, + {file = "winrt_Windows.Devices.Bluetooth.Advertisement-2.0.0b1-cp39-cp39-win_arm64.whl", hash = "sha256:b30ab9b8c1ecf818be08bac86bee425ef40f75060c4011d4e6c2e624a7b9916e"}, +] + +[package.dependencies] +winrt-runtime = "2.0.0-beta.1" + +[package.extras] +all = ["winrt-Windows.Devices.Bluetooth[all] (==2.0.0-beta.1)", "winrt-Windows.Foundation.Collections[all] (==2.0.0-beta.1)", "winrt-Windows.Foundation[all] (==2.0.0-beta.1)", "winrt-Windows.Storage.Streams[all] (==2.0.0-beta.1)"] + +[[package]] +name = "winrt-windows-devices-bluetooth-genericattributeprofile" +version = "2.0.0b1" +description = "Python projection of Windows Runtime (WinRT) APIs" +optional = true +python-versions = "<3.13,>=3.9" +files = [ + {file = "winrt-Windows.Devices.Bluetooth.GenericAttributeProfile-2.0.0b1.tar.gz", hash = "sha256:93b745d51ecfb3e9d3a21623165cc065735c9e0146cb7a26744182c164e63e14"}, + {file = "winrt_Windows.Devices.Bluetooth.GenericAttributeProfile-2.0.0b1-cp310-cp310-win32.whl", hash = "sha256:db740aaedd80cca5b1a390663b26c7733eb08f4c57ade6a04b055d548e9d042b"}, + {file = "winrt_Windows.Devices.Bluetooth.GenericAttributeProfile-2.0.0b1-cp310-cp310-win_amd64.whl", hash = "sha256:7c81aa6c066cdab58bcc539731f208960e094a6d48b59118898e1e804dbbdf7f"}, + {file = "winrt_Windows.Devices.Bluetooth.GenericAttributeProfile-2.0.0b1-cp310-cp310-win_arm64.whl", hash = "sha256:92277a6bbcbe2225ad1be92968af597dc77bc37a63cd729690d2d9fb5094ae25"}, + {file = "winrt_Windows.Devices.Bluetooth.GenericAttributeProfile-2.0.0b1-cp311-cp311-win32.whl", hash = "sha256:6b48209669c1e214165530793cf9916ae44a0ae2618a9be7a489e8c94f7e745f"}, + {file = "winrt_Windows.Devices.Bluetooth.GenericAttributeProfile-2.0.0b1-cp311-cp311-win_amd64.whl", hash = "sha256:2f17216e6ce748eaef02fb0658213515d3ff31e2dbb18f070a614876f818c90d"}, + {file = "winrt_Windows.Devices.Bluetooth.GenericAttributeProfile-2.0.0b1-cp311-cp311-win_arm64.whl", hash = "sha256:db798a0f0762e390da5a9f02f822daff00692bd951a492224bf46782713b2938"}, + {file = "winrt_Windows.Devices.Bluetooth.GenericAttributeProfile-2.0.0b1-cp312-cp312-win32.whl", hash = "sha256:b8d9dba04b9cfa53971c35117fc3c68c94bfa5e2ed18ce680f731743598bf246"}, + {file = "winrt_Windows.Devices.Bluetooth.GenericAttributeProfile-2.0.0b1-cp312-cp312-win_amd64.whl", hash = "sha256:e5260b3f33dee8a896604297e05efc04d04298329c205a74ded8e2d6333e84b7"}, + {file = "winrt_Windows.Devices.Bluetooth.GenericAttributeProfile-2.0.0b1-cp312-cp312-win_arm64.whl", hash = "sha256:822ef539389ecb546004345c4dce8b9b7788e2e99a1d6f0947a4b123dceb7fed"}, + {file = "winrt_Windows.Devices.Bluetooth.GenericAttributeProfile-2.0.0b1-cp39-cp39-win32.whl", hash = "sha256:11e6863e7a94d2b6dd76ddcd19c01e311895810a4ce6ad08c7b5534294753243"}, + {file = "winrt_Windows.Devices.Bluetooth.GenericAttributeProfile-2.0.0b1-cp39-cp39-win_amd64.whl", hash = "sha256:20de8d04c301c406362c93e78d41912aea0af23c4b430704aba329420d7c2cdf"}, + {file = "winrt_Windows.Devices.Bluetooth.GenericAttributeProfile-2.0.0b1-cp39-cp39-win_arm64.whl", hash = "sha256:918059796f2f123216163b928ecde8ecec17994fb7a94042af07fda82c132a6d"}, +] + +[package.dependencies] +winrt-runtime = "2.0.0-beta.1" + +[package.extras] +all = ["winrt-Windows.Devices.Bluetooth[all] (==2.0.0-beta.1)", "winrt-Windows.Devices.Enumeration[all] (==2.0.0-beta.1)", "winrt-Windows.Foundation.Collections[all] (==2.0.0-beta.1)", "winrt-Windows.Foundation[all] (==2.0.0-beta.1)", "winrt-Windows.Storage.Streams[all] (==2.0.0-beta.1)"] + +[[package]] +name = "winrt-windows-devices-enumeration" +version = "2.0.0b1" +description = "Python projection of Windows Runtime (WinRT) APIs" +optional = true +python-versions = "<3.13,>=3.9" +files = [ + {file = "winrt-Windows.Devices.Enumeration-2.0.0b1.tar.gz", hash = "sha256:8f214040e4edbe57c4943488887db89f4a00d028c34169aafd2205e228026100"}, + {file = "winrt_Windows.Devices.Enumeration-2.0.0b1-cp310-cp310-win32.whl", hash = "sha256:dcb9e7d230aefec8531a46d393ecb1063b9d4b97c9f3ff2fc537ce22bdfa2444"}, + {file = "winrt_Windows.Devices.Enumeration-2.0.0b1-cp310-cp310-win_amd64.whl", hash = "sha256:22a3e1fef40786cc8d51320b6f11ff25de6c674475f3ba608a46915e1dadf0f5"}, + {file = "winrt_Windows.Devices.Enumeration-2.0.0b1-cp310-cp310-win_arm64.whl", hash = "sha256:2edcfeb70a71d40622873cad96982a28e92a7ee71f33968212dd3598b2d8d469"}, + {file = "winrt_Windows.Devices.Enumeration-2.0.0b1-cp311-cp311-win32.whl", hash = "sha256:ce4eb88add7f5946d2666761a97a3bb04cac2a061d264f03229c1e15dbd7ce91"}, + {file = "winrt_Windows.Devices.Enumeration-2.0.0b1-cp311-cp311-win_amd64.whl", hash = "sha256:a9001f17991572abdddab7ab074e08046e74e05eeeaf3b2b01b8b47d2879b64c"}, + {file = "winrt_Windows.Devices.Enumeration-2.0.0b1-cp311-cp311-win_arm64.whl", hash = "sha256:0440b91ce144111e207f084cec6b1277162ef2df452d321951e989ce87dc9ced"}, + {file = "winrt_Windows.Devices.Enumeration-2.0.0b1-cp312-cp312-win32.whl", hash = "sha256:e4fae13126f13a8d9420b74fb5a5ff6a6b2f91f7718c4be2d4a8dc1337c58f59"}, + {file = "winrt_Windows.Devices.Enumeration-2.0.0b1-cp312-cp312-win_amd64.whl", hash = "sha256:e352eebc23dc94fb79e67a056c057fb0e16c20c8cb881dc826094c20ed4791e3"}, + {file = "winrt_Windows.Devices.Enumeration-2.0.0b1-cp312-cp312-win_arm64.whl", hash = "sha256:b43f5c1f053a170e6e4b44ba69838ac223f9051adca1a56506d4c46e98d1485f"}, + {file = "winrt_Windows.Devices.Enumeration-2.0.0b1-cp39-cp39-win32.whl", hash = "sha256:ed245fad8de6a134d5c3a630204e7f8238aa944a40388005bce0ce3718c410fa"}, + {file = "winrt_Windows.Devices.Enumeration-2.0.0b1-cp39-cp39-win_amd64.whl", hash = "sha256:22a9eefdbfe520778512266d0b48ff239eaa8d272fce6f5cb1ff352bed0619f4"}, + {file = "winrt_Windows.Devices.Enumeration-2.0.0b1-cp39-cp39-win_arm64.whl", hash = "sha256:397d43f8fd2621a7719b9eab6a4a8e72a1d6fa2d9c36525a30812f8e7bad3bdf"}, +] + +[package.dependencies] +winrt-runtime = "2.0.0-beta.1" + +[package.extras] +all = ["winrt-Windows.ApplicationModel.Background[all] (==2.0.0-beta.1)", "winrt-Windows.Foundation.Collections[all] (==2.0.0-beta.1)", "winrt-Windows.Foundation[all] (==2.0.0-beta.1)", "winrt-Windows.Security.Credentials[all] (==2.0.0-beta.1)", "winrt-Windows.Storage.Streams[all] (==2.0.0-beta.1)", "winrt-Windows.UI.Popups[all] (==2.0.0-beta.1)", "winrt-Windows.UI[all] (==2.0.0-beta.1)"] + +[[package]] +name = "winrt-windows-foundation" +version = "2.0.0b1" +description = "Python projection of Windows Runtime (WinRT) APIs" +optional = true +python-versions = "<3.13,>=3.9" +files = [ + {file = "winrt-Windows.Foundation-2.0.0b1.tar.gz", hash = "sha256:976b6da942747a7ca5a179a35729d8dc163f833e03b085cf940332a5e9070d54"}, + {file = "winrt_Windows.Foundation-2.0.0b1-cp310-cp310-win32.whl", hash = "sha256:5337ac1ec260132fbff868603e73a3738d4001911226e72669b3d69c8a256d5e"}, + {file = "winrt_Windows.Foundation-2.0.0b1-cp310-cp310-win_amd64.whl", hash = "sha256:af969e5bb9e2e41e4e86a361802528eafb5eb8fe87ec1dba6048c0702d63caa8"}, + {file = "winrt_Windows.Foundation-2.0.0b1-cp310-cp310-win_arm64.whl", hash = "sha256:bbbfa6b3c444a1074a630fd4a1b71171be7a5c9bb07c827ad9259fadaed56cf2"}, + {file = "winrt_Windows.Foundation-2.0.0b1-cp311-cp311-win32.whl", hash = "sha256:b91bd92b1854c073acd81aa87cf8df571d2151b1dd050b6181aa36f7acc43df4"}, + {file = "winrt_Windows.Foundation-2.0.0b1-cp311-cp311-win_amd64.whl", hash = "sha256:2f5359f25703347e827dbac982150354069030f1deecd616f7ce37ad90cbcb00"}, + {file = "winrt_Windows.Foundation-2.0.0b1-cp311-cp311-win_arm64.whl", hash = "sha256:0f1f1978173ddf0ee6262c2edb458f62d628b9fa0df10cd1e8c78c833af3197e"}, + {file = "winrt_Windows.Foundation-2.0.0b1-cp312-cp312-win32.whl", hash = "sha256:c1d23b737f733104b91c89c507b58d0b3ef5f3234a1b608ef6dfb6dbbb8777ea"}, + {file = "winrt_Windows.Foundation-2.0.0b1-cp312-cp312-win_amd64.whl", hash = "sha256:95de6c29e9083fe63f127b965b54dfa52a6424a93a94ce87cfad4c1900a6e887"}, + {file = "winrt_Windows.Foundation-2.0.0b1-cp312-cp312-win_arm64.whl", hash = "sha256:4707063a5a6980e3f71aebeea5ac93101c753ec13a0b47be9ea4dbc0d5ff361e"}, + {file = "winrt_Windows.Foundation-2.0.0b1-cp39-cp39-win32.whl", hash = "sha256:d0259f1f4a1b8e20d0cbd935a889c0f7234f720645590260f9cf3850fdc1e1fa"}, + {file = "winrt_Windows.Foundation-2.0.0b1-cp39-cp39-win_amd64.whl", hash = "sha256:15c7b324d0f59839fb4492d84bb1c870881c5c67cb94ac24c664a7c4dce1c475"}, + {file = "winrt_Windows.Foundation-2.0.0b1-cp39-cp39-win_arm64.whl", hash = "sha256:16ad741f4d38e99f8409ba5760299d0052003255f970f49f4b8ba2e0b609c8b7"}, +] + +[package.dependencies] +winrt-runtime = "2.0.0-beta.1" + +[package.extras] +all = ["winrt-Windows.Foundation.Collections[all] (==2.0.0-beta.1)"] + +[[package]] +name = "winrt-windows-foundation-collections" +version = "2.0.0b1" +description = "Python projection of Windows Runtime (WinRT) APIs" +optional = true +python-versions = "<3.13,>=3.9" +files = [ + {file = "winrt-Windows.Foundation.Collections-2.0.0b1.tar.gz", hash = "sha256:185d30f8103934124544a40aac005fa5918a9a7cb3179f45e9863bb86e22ad43"}, + {file = "winrt_Windows.Foundation.Collections-2.0.0b1-cp310-cp310-win32.whl", hash = "sha256:042142e916a170778b7154498aae61254a1a94c552954266b73479479d24f01d"}, + {file = "winrt_Windows.Foundation.Collections-2.0.0b1-cp310-cp310-win_amd64.whl", hash = "sha256:9f68e66055121fc1e04c4fda627834aceee6fbe922e77d6ccaecf9582e714c57"}, + {file = "winrt_Windows.Foundation.Collections-2.0.0b1-cp310-cp310-win_arm64.whl", hash = "sha256:a4609411263cc7f5e93a9a5677b21e2ef130e26f9030bfa960b3e82595324298"}, + {file = "winrt_Windows.Foundation.Collections-2.0.0b1-cp311-cp311-win32.whl", hash = "sha256:5296858aa44c53936460a119794b80eedd6bd094016c1bf96822f92cb95ea419"}, + {file = "winrt_Windows.Foundation.Collections-2.0.0b1-cp311-cp311-win_amd64.whl", hash = "sha256:3db1e1c80c97474e7c88b6052bd8982ca61723fd58ace11dc91a5522662e0b2a"}, + {file = "winrt_Windows.Foundation.Collections-2.0.0b1-cp311-cp311-win_arm64.whl", hash = "sha256:c3a594e660c59f9fab04ae2f40bda7c809e8ec4748bada4424dfb02b43d4bfe1"}, + {file = "winrt_Windows.Foundation.Collections-2.0.0b1-cp312-cp312-win32.whl", hash = "sha256:0f355ee943ec5b835e694d97e9e93545a42d6fb984a61f442467789550d62c3f"}, + {file = "winrt_Windows.Foundation.Collections-2.0.0b1-cp312-cp312-win_amd64.whl", hash = "sha256:c4a0cd2eb9f47c7ca3b66d12341cc822250bf26854a93fd58ab77f7a48dfab3a"}, + {file = "winrt_Windows.Foundation.Collections-2.0.0b1-cp312-cp312-win_arm64.whl", hash = "sha256:744dbef50e8b8f34904083cae9ad43ac6e28facb9e166c4f123ce8e758141067"}, + {file = "winrt_Windows.Foundation.Collections-2.0.0b1-cp39-cp39-win32.whl", hash = "sha256:b7c767184aec3a3d7cba2cd84fadcd68106854efabef1a61092052294d6d6f4f"}, + {file = "winrt_Windows.Foundation.Collections-2.0.0b1-cp39-cp39-win_amd64.whl", hash = "sha256:7c1ffe99c12f14fc4ab7027757780e6d850fa2fb23ec404a54311fbd9f1970d3"}, + {file = "winrt_Windows.Foundation.Collections-2.0.0b1-cp39-cp39-win_arm64.whl", hash = "sha256:870fa040ed36066e4c240c35973d8b2e0d7c38cc6050a42d993715ec9e3b748c"}, +] + +[package.dependencies] +winrt-runtime = "2.0.0-beta.1" + +[package.extras] +all = ["winrt-Windows.Foundation[all] (==2.0.0-beta.1)"] + +[[package]] +name = "winrt-windows-storage-streams" +version = "2.0.0b1" +description = "Python projection of Windows Runtime (WinRT) APIs" +optional = true +python-versions = "<3.13,>=3.9" +files = [ + {file = "winrt-Windows.Storage.Streams-2.0.0b1.tar.gz", hash = "sha256:029d67cdc9b092d56c682740fe3c42f267dc5d3346b5c0b12ebc03f38e7d2f1f"}, + {file = "winrt_Windows.Storage.Streams-2.0.0b1-cp310-cp310-win32.whl", hash = "sha256:49c90d4bfd539f6676226dfcb4b3574ddd6be528ffc44aa214c55af88c2de89e"}, + {file = "winrt_Windows.Storage.Streams-2.0.0b1-cp310-cp310-win_amd64.whl", hash = "sha256:22cc82779cada84aa2633841e25b33f3357737d912a1d9ecc1ee5a8b799b5171"}, + {file = "winrt_Windows.Storage.Streams-2.0.0b1-cp310-cp310-win_arm64.whl", hash = "sha256:b1750a111be32466f4f0781cbb5df195ac940690571dff4564492b921b162563"}, + {file = "winrt_Windows.Storage.Streams-2.0.0b1-cp311-cp311-win32.whl", hash = "sha256:e79b1183ab26d9b95cf3e6dbe3f488a40605174a5a112694dbb7dbfb50899daf"}, + {file = "winrt_Windows.Storage.Streams-2.0.0b1-cp311-cp311-win_amd64.whl", hash = "sha256:3e90a1207eb3076f051a7785132f7b056b37343a68e9481a50c6defb3f660099"}, + {file = "winrt_Windows.Storage.Streams-2.0.0b1-cp311-cp311-win_arm64.whl", hash = "sha256:4da06522b4fa9cfcc046b604cc4aa1c6a887cc4bb5b8a637ed9bff8028a860bb"}, + {file = "winrt_Windows.Storage.Streams-2.0.0b1-cp312-cp312-win32.whl", hash = "sha256:6f74f8ab8ac0d8de61c709043315361d8ac63f8144f3098d428472baadf8246a"}, + {file = "winrt_Windows.Storage.Streams-2.0.0b1-cp312-cp312-win_amd64.whl", hash = "sha256:5cf7c8d67836c60392d167bfe4f98ac7abcb691bfba2d19e322d0f9181f58347"}, + {file = "winrt_Windows.Storage.Streams-2.0.0b1-cp312-cp312-win_arm64.whl", hash = "sha256:f7f679f2c0f71791eca835856f57942ee5245094c1840a6c34bc7c2176b1bcd6"}, + {file = "winrt_Windows.Storage.Streams-2.0.0b1-cp39-cp39-win32.whl", hash = "sha256:5beb53429fa9a11ede56b4a7cefe28c774b352dd355f7951f2a4dd7e9ec9b39a"}, + {file = "winrt_Windows.Storage.Streams-2.0.0b1-cp39-cp39-win_amd64.whl", hash = "sha256:f84233c4b500279d8f5840cb8c47776bc040fcecba05c6c9ab9767053698fc8b"}, + {file = "winrt_Windows.Storage.Streams-2.0.0b1-cp39-cp39-win_arm64.whl", hash = "sha256:cfb163ddbb435906f75ef92a768573b0190e194e1438cea5a4c1d4d32a6b9386"}, +] + +[package.dependencies] +winrt-runtime = "2.0.0-beta.1" + +[package.extras] +all = ["winrt-Windows.Foundation.Collections[all] (==2.0.0-beta.1)", "winrt-Windows.Foundation[all] (==2.0.0-beta.1)", "winrt-Windows.Storage[all] (==2.0.0-beta.1)", "winrt-Windows.System[all] (==2.0.0-beta.1)"] + [[package]] name = "yarl" version = "1.9.2" @@ -766,7 +1159,10 @@ files = [ idna = ">=2.0" multidict = ">=4.0" +[extras] +ble = ["bleak", "dbus-fast"] + [metadata] lock-version = "2.0" -python-versions = "^3.9" -content-hash = "7ebf23dc31bfac4912c32eaac5765e2754e37cf722f56c9fde8e6680810575b1" +python-versions = ">=3.9, <3.13" +content-hash = "dc978710b69b079be094aa48da955f8c1c8a5a719992a27e63c70f6f2820ff29" diff --git a/pyproject.toml b/pyproject.toml index e69f50a..9969f10 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -14,7 +14,8 @@ python = ">=3.9, <3.13" aiohttp = ">=3.0.0" cryptography = "^41.0.1" backports-strenum = { version = "^1.2.4", python = "<3.11" } -bleak = "^0.21.1" +bleak = { version = "^0.21", optional = true } +dbus-fast = {version = "^2.14.0", optional = true, platform = "linux"} [tool.poetry.group.dev.dependencies] pytest = "^7.1.2" @@ -22,6 +23,9 @@ pytest-asyncio = "^0.18.3" python-dotenv = "^0.20.0" aresponses = "^2.1.5" +[tool.poetry.extras] +ble = ["bleak", "dbus-fast"] + [tool.poetry-dynamic-versioning] enable = true vcs = "git" diff --git a/src/rivian/ble.py b/src/rivian/ble.py index 08a411f..5e156a8 100644 --- a/src/rivian/ble.py +++ b/src/rivian/ble.py @@ -1,86 +1,168 @@ """Rivian BLE handler.""" from __future__ import annotations -import platform + import asyncio -import secrets import logging +import platform +import secrets -from bleak import BleakClient, BLEDevice -from rivian import utils +from .utils import generate_ble_command_hmac _LOGGER = logging.getLogger(__name__) +try: + from bleak import BleakClient, BleakScanner, BLEDevice +except ImportError: + _LOGGER.error("Please install 'rivian-python-client[ble]' to use BLE features.") + raise + + +DEVICE_LOCAL_NAME = "Rivian Phone Key" + +ACTIVE_ENTRY_CHARACTERISTIC_UUID = "5249565F-4D4F-424B-4559-5F5752495445" PHONE_ID_VEHICLE_ID_UUID = "AA49565A-4D4F-424B-4559-5F5752495445" -PNONCE_VNONCE_UUID = "E020A15D-E730-4B2C-908B-51DAF9D41E19" -BLE_ACTIVE_ENTRY_UUID = "5249565F-4D4F-424B-4559-5F5752495445" +PHONE_NONCE_VEHICLE_NONCE_UUID = "E020A15D-E730-4B2C-908B-51DAF9D41E19" -NOTIFICATION_TIMEOUT = 3.0 CONNECT_TIMEOUT = 10.0 +NOTIFICATION_TIMEOUT = 3.0 -async def pair_phone( - device: BLEDevice, vehicle_id: str, phone_id: str, vehicle_key: str, private_key: str -) -> bool: - success = False - # Create an asyncio.Event object to signal the arrival of a new notification. - vid_event = asyncio.Event() - nonce_event = asyncio.Event() - notification_data: bytearray | None = None +class BleNotificationResponse: + """BLE notification response helper.""" - # Callback for notifications from vehicle characteristics - def id_notification_handler(_, data: bytearray) -> None: - nonlocal notification_data - notification_data = data - vid_event.set() + def __init__(self) -> None: + """Initialize the BLE notification response helper.""" + self.data: bytes | None = None + self.event = asyncio.Event() - def nonce_notification_handler(_, data: bytearray) -> None: - nonlocal notification_data - notification_data = data - nonce_event.set() + def notification_handler(self, _, notification_data: bytearray) -> None: + """Notification handler.""" + self.data = notification_data + self.event.set() - # this is a dummy callback (unused) - def active_entry_notification_handler(_, data: bytearray) -> None: - pass + async def wait(self, timeout: float | None = NOTIFICATION_TIMEOUT) -> bool: + """Wait for the notification response.""" + return await asyncio.wait_for(self.event.wait(), timeout) - try: - _LOGGER.debug("Connecting to %s [%s]", BLEDevice.name, BLEDevice.address) - async with BleakClient(device, timeout=CONNECT_TIMEOUT) as client: - _LOGGER.debug("Connected to %s [%s]", BLEDevice.name, BLEDevice.address) - await client.start_notify(PHONE_ID_VEHICLE_ID_UUID, id_notification_handler) - await client.start_notify(PNONCE_VNONCE_UUID, nonce_notification_handler) - # wait to enable notifications for BLE_ACTIVE_ENTRY_UUID - # write the phone ID (16-bytes) response will be vehicle ID - await client.write_gatt_char(PHONE_ID_VEHICLE_ID_UUID, bytes.fromhex(phone_id.replace("-", ""))) - await asyncio.wait_for(vid_event.wait(), NOTIFICATION_TIMEOUT) +async def create_notification_handler( + client: BleakClient, char_specifier: str +) -> BleNotificationResponse: + """Create a notification handler.""" + response = BleNotificationResponse() + await client.start_notify(char_specifier, response.notification_handler) + return response + - vas_vehicle_id = notification_data.hex() - if vas_vehicle_id != vehicle_id.replace("-", ""): +async def pair_phone( + device: BLEDevice, + vas_vehicle_id: str, + phone_id: str, + vehicle_public_key: str, + private_key: str, +) -> bool: + """Pair a phone locally via BLE. + + The phone must first be enrolled via `rivian.enroll_phone`. + This finishes the process to enable cloud and local vehicle control. + """ + _LOGGER.debug("Connecting to %s", device) + try: + async with BleakClient(device, timeout=CONNECT_TIMEOUT) as client: + _LOGGER.debug("Connected to %s", device) + vehicle_id_handler = await create_notification_handler( + client, PHONE_ID_VEHICLE_ID_UUID + ) + nonce_handler = await create_notification_handler( + client, PHONE_NONCE_VEHICLE_NONCE_UUID + ) + + _LOGGER.debug("Validating id") + await client.write_gatt_char( + PHONE_ID_VEHICLE_ID_UUID, bytes.fromhex(phone_id.replace("-", "")) + ) + await vehicle_id_handler.wait() + + assert vehicle_id_handler.data + vehicle_id_response = vehicle_id_handler.data.hex() + if vehicle_id_response != vas_vehicle_id.replace("-", ""): _LOGGER.debug( - "Incorrect vas vehicle id: received %s, expected %s", + "Incorrect vehicle id: received %s, expected %s", + vehicle_id_response, vas_vehicle_id, - vehicle_id) + ) return False - # generate pnonce (16-bytes random) - pnonce = secrets.token_bytes(16) - hmac = utils.generate_ble_command_hmac(pnonce, vehicle_key, private_key) - - # write pnonce (48-bytes) response will be vnonce - await client.write_gatt_char(PNONCE_VNONCE_UUID, pnonce + hmac ) - await asyncio.wait_for(nonce_event.wait(), NOTIFICATION_TIMEOUT) - - # vehicle is authenticated, trigger bonding + _LOGGER.debug("Exchanging nonce") + phone_nonce = secrets.token_bytes(16) + hmac = generate_ble_command_hmac( + phone_nonce, vehicle_public_key, private_key + ) + await client.write_gatt_char( + PHONE_NONCE_VEHICLE_NONCE_UUID, phone_nonce + hmac + ) + await nonce_handler.wait() + + # Vehicle is authenticated, trigger bonding + _LOGGER.debug("Attempting to pair") if platform.system() == "Darwin": # Mac BLE API doesn't have an explicit way to trigger bonding - # enable notification on BLE_ACTIVE_ENTRY_UUID to trigger bonding - await client.start_notify(BLE_ACTIVE_ENTRY_UUID, active_entry_notification_handler) + # Instead, enable notification on protected characteristic to trigger bonding manually + await client.start_notify( + ACTIVE_ENTRY_CHARACTERISTIC_UUID, lambda _, __: None + ) else: await client.pair() - - success = True - except Exception as e: - _LOGGER.debug("An exception occurred while pairing: %s", str(e)) + _LOGGER.debug("Successfully paired with %s", device) + return True + except Exception as ex: # pylint: disable=broad-except + _LOGGER.debug( + "Couldn't connect to %s. " + 'Make sure you are in the correct vehicle and have selected "Set Up" for the appropriate key and try again' + "%s", + device, + ("" if isinstance(ex, asyncio.TimeoutError) else f": {ex}"), + ) + return False + - return success +async def find_phone_key() -> BLEDevice | None: + """Find phone key.""" + async with BleakScanner() as scanner: + return await scanner.find_device_by_name(DEVICE_LOCAL_NAME) + + +async def set_bluez_pairable(device: BLEDevice) -> bool: + """Set bluez to pairable on Linux systems.""" + if (_os := platform.system()) != "Linux": + raise OSError(f"BlueZ is not available on {_os}-based systems") + + # pylint: disable=import-error, import-outside-toplevel + from dbus_fast import BusType # type: ignore + from dbus_fast.aio import MessageBus # type: ignore + + try: + path = device.details["props"]["Adapter"] + except Exception: # pylint: disable=broad-except + path = "/org/bluez/hci0" + _LOGGER.warning( + "Couldn't determine BT controller path, defaulting to %s: %s", + path, + device.details, + exc_info=True, + ) + + try: + bus = await MessageBus(bus_type=BusType.SYSTEM).connect() + introspection = await bus.introspect("org.bluez", path) + pobject = bus.get_proxy_object("org.bluez", path, introspection) + iface = pobject.get_interface("org.bluez.Adapter1") + if not await iface.get_pairable(): + await iface.set_pairable(True) + bus.disconnect() + except Exception as ex: # pylint: disable=broad-except + _LOGGER.error(ex) + return False + + return True diff --git a/src/rivian/rivian.py b/src/rivian/rivian.py index 0ebf3ff..26a9063 100644 --- a/src/rivian/rivian.py +++ b/src/rivian/rivian.py @@ -248,10 +248,12 @@ async def enroll_phone( device_name: str, public_key: str, ) -> bool: - """Enable control of a vehicle by enrolling a phone. + """Enroll a phone. To generate a public/private key for enrollment, use the `utils.generate_key_pair` function. The private key will need to be retained to sign commands sent via the `send_vehicle_command` method. + To enable vehicle control, the phone will then also need to be paired locally via BLE, + which can be done via `ble.pair_phone`. """ url = GRAPHQL_GATEWAY headers = BASE_HEADERS | { diff --git a/src/rivian/utils.py b/src/rivian/utils.py index ccee44e..6805d38 100644 --- a/src/rivian/utils.py +++ b/src/rivian/utils.py @@ -66,13 +66,15 @@ def generate_key_pair() -> tuple[str, str]: # Return the public-private key pair as strings return (public_key_str, private_key_str) -def generate_ble_command_hmac( - hmac_data: bytes, vehicle_key: str, private_key: str - ): + +def generate_ble_command_hmac( + hmac_data: bytes, vehicle_public_key: str, private_key: str +) -> bytes: """Generate ble command hmac.""" - secret_key = get_secret_key(private_key, vehicle_key) + secret_key = get_secret_key(private_key, vehicle_public_key) return bytes.fromhex(get_message_signature(secret_key, hmac_data)) + def generate_vehicle_command_hmac( command: str, timestamp: str, vehicle_key: str, private_key: str ): From 9236a2c509c08262dc3cf196b7ec6b0ea979fb73 Mon Sep 17 00:00:00 2001 From: Nathan Spencer Date: Sat, 11 Nov 2023 16:33:47 -0700 Subject: [PATCH 10/11] Keep vehicle_key as parameter name --- src/rivian/ble.py | 6 ++---- src/rivian/utils.py | 4 ++-- 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/src/rivian/ble.py b/src/rivian/ble.py index 5e156a8..f573cfa 100644 --- a/src/rivian/ble.py +++ b/src/rivian/ble.py @@ -58,7 +58,7 @@ async def pair_phone( device: BLEDevice, vas_vehicle_id: str, phone_id: str, - vehicle_public_key: str, + vehicle_key: str, private_key: str, ) -> bool: """Pair a phone locally via BLE. @@ -95,9 +95,7 @@ async def pair_phone( _LOGGER.debug("Exchanging nonce") phone_nonce = secrets.token_bytes(16) - hmac = generate_ble_command_hmac( - phone_nonce, vehicle_public_key, private_key - ) + hmac = generate_ble_command_hmac(phone_nonce, vehicle_key, private_key) await client.write_gatt_char( PHONE_NONCE_VEHICLE_NONCE_UUID, phone_nonce + hmac ) diff --git a/src/rivian/utils.py b/src/rivian/utils.py index 6805d38..7790f58 100644 --- a/src/rivian/utils.py +++ b/src/rivian/utils.py @@ -68,10 +68,10 @@ def generate_key_pair() -> tuple[str, str]: def generate_ble_command_hmac( - hmac_data: bytes, vehicle_public_key: str, private_key: str + hmac_data: bytes, vehicle_key: str, private_key: str ) -> bytes: """Generate ble command hmac.""" - secret_key = get_secret_key(private_key, vehicle_public_key) + secret_key = get_secret_key(private_key, vehicle_key) return bytes.fromhex(get_message_signature(secret_key, hmac_data)) From 156872d946f91766f8badb671768dd61af9adf3a Mon Sep 17 00:00:00 2001 From: Nathan Spencer Date: Sat, 11 Nov 2023 17:34:28 -0700 Subject: [PATCH 11/11] Adjust parameter order --- src/rivian/ble.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/rivian/ble.py b/src/rivian/ble.py index f573cfa..a063357 100644 --- a/src/rivian/ble.py +++ b/src/rivian/ble.py @@ -56,8 +56,8 @@ async def create_notification_handler( async def pair_phone( device: BLEDevice, - vas_vehicle_id: str, phone_id: str, + vas_vehicle_id: str, vehicle_key: str, private_key: str, ) -> bool: