class BMWConnectedDriveAccount: """Representation of a BMW vehicle.""" def __init__(self, username: str, password: str, region_str: str, name: str, read_only) -> None: """Constructor.""" from bimmer_connected.account import ConnectedDriveAccount from bimmer_connected.country_selector import get_region_from_name region = get_region_from_name(region_str) self.read_only = read_only self.account = ConnectedDriveAccount(username, password, region) self.name = name self._update_listeners = [] def update(self, *_): """Update the state of all vehicles. Notify all listeners about the update. """ _LOGGER.debug('Updating vehicle state for account %s, ' 'notifying %d listeners', self.name, len(self._update_listeners)) try: self.account.update_vehicle_states() for listener in self._update_listeners: listener() except IOError as exception: _LOGGER.error('Error updating the vehicle state.') _LOGGER.exception(exception) def add_update_listener(self, listener): """Add a listener for update notifications.""" self._update_listeners.append(listener)
def __init__(self, username: str, password: str, country: str, name: str) -> None: """Constructor.""" from bimmer_connected.account import ConnectedDriveAccount self.account = ConnectedDriveAccount(username, password, country) self.name = name self._update_listeners = []
def test_check_control_messages(self): """Test handling of check control messages. F48 is the only vehicle with active Check Control Messages, so we only expect to get something there. """ backend_mock = BackendMock() with mock.patch('bimmer_connected.account.requests', new=backend_mock): backend_mock.setup_default_vehicles() account = ConnectedDriveAccount(TEST_USERNAME, TEST_PASSWORD, TEST_REGION) account.update_vehicle_states() for vehicle in account.vehicles: print(vehicle.name, vehicle.vin) if vehicle.vin == F48_VIN: self.assertTrue(vehicle.state.has_check_control_messages) else: self.assertFalse(vehicle.state.has_check_control_messages)
def test_parsing_of_lsc_type(self): """Test parsing the lsc type field.""" backend_mock = BackendMock() with mock.patch('bimmer_connected.account.requests', new=backend_mock): account = ConnectedDriveAccount(TEST_USERNAME, TEST_PASSWORD, TEST_REGION) for vehicle in account.vehicles: self.assertIsNotNone(vehicle.lsc_type)
def test_trigger_remote_services(self): """Test executing a remote light flash.""" remote_services._POLLING_CYCLE = 0 remote_services._UPDATE_AFTER_REMOTE_SERVICE_DELAY = 0 services = [('RLF', 'trigger_remote_light_flash', False), ('RDL', 'trigger_remote_door_lock', True), ('RDU', 'trigger_remote_door_unlock', True), ('RCN', 'trigger_remote_air_conditioning', True), ('RHB', 'trigger_remote_horn', False)] for service, call, triggers_update in services: backend_mock = BackendMock() backend_mock.setup_default_vehicles() backend_mock.add_response( r'.*/api/vehicle/remoteservices/v1/{vin}/{service}'.format( vin=G31_VIN, service=service), data_files=['G31_NBTevo/RLF_INITIAL_RESPONSE.json']) backend_mock.add_response( '.*/api/vehicle/remoteservices/v1/{vin}/state/execution'. format(vin=G31_VIN), data_files=[ 'G31_NBTevo/RLF_PENDING.json', 'G31_NBTevo/RLF_DELIVERED.json', 'G31_NBTevo/RLF_EXECUTED.json' ]) with mock.patch('bimmer_connected.account.requests', new=backend_mock): account = ConnectedDriveAccount(TEST_USERNAME, TEST_PASSWORD, TEST_COUNTRY) mock_listener = mock.Mock(return_value=None) account.add_update_listener(mock_listener) vehicle = account.get_vehicle(G31_VIN) response = getattr(vehicle.remote_services, call)() self.assertEqual(ExecutionState.EXECUTED, response.state) if triggers_update: mock_listener.assert_called_once_with() else: mock_listener.assert_not_called()
def test_us_header(self): """Test if the host is set correctly in the request.""" backend_mock = BackendMock() with mock.patch('bimmer_connected.account.requests', new=backend_mock): ConnectedDriveAccount(TEST_USERNAME, TEST_PASSWORD, Regions.NORTH_AMERICA) request = [ r for r in backend_mock.last_request if 'oauth' in r.url ][0] self.assertEqual('b2vapi.bmwgroup.us', request.headers['Host'])
def test_server_error(self): """Test parsing the results of a server error.""" with requests_mock.Mocker(adapter=get_base_adapter()) as mock: mock.post( "/gcdm/oauth/authenticate", text=load_response(RESPONSE_DIR / "auth" / "auth_error_internal_error.txt"), status_code=500, ) with self.assertRaises(HTTPError): ConnectedDriveAccount(TEST_USERNAME, TEST_PASSWORD, TEST_REGION)
def test_invalid_password_china(self): """Test parsing the results of an invalid password.""" with requests_mock.Mocker(adapter=get_base_adapter()) as mock: mock.post( "/eadrax-coas/v1/login/pwd", json=load_response(RESPONSE_DIR / "auth" / "auth_cn_login_error.json"), status_code=422, ) with self.assertRaises(HTTPError): ConnectedDriveAccount(TEST_USERNAME, TEST_PASSWORD, get_region_from_name("china"))
def test_invalid_password(self): """Test parsing the results of an invalid password.""" with requests_mock.Mocker(adapter=get_base_adapter()) as mock: mock.post( "/gcdm/oauth/authenticate", json=load_response(RESPONSE_DIR / "auth" / "auth_error_wrong_password.json"), status_code=401, ) with self.assertRaises(HTTPError): ConnectedDriveAccount(TEST_USERNAME, TEST_PASSWORD, TEST_REGION)
def test_trigger_remote_services(self): """Test executing a remote light flash.""" remote_services._POLLING_CYCLE = 0 remote_services._UPDATE_AFTER_REMOTE_SERVICE_DELAY = 0 services = [ ('LIGHT_FLASH', 'trigger_remote_light_flash', False), ('DOOR_LOCK', 'trigger_remote_door_lock', True), ('DOOR_UNLOCK', 'trigger_remote_door_unlock', True), ('CLIMATE_NOW', 'trigger_remote_air_conditioning', True), ('HORN_BLOW', 'trigger_remote_horn', False) ] for service, call, triggers_update in services: backend_mock = BackendMock() backend_mock.setup_default_vehicles() backend_mock.add_response('https://.+/webapi/v1/user/vehicles/{vin}/executeService'.format(vin=G31_VIN), data_files=[_RESPONSE_INITIATED]) backend_mock.add_response( r'https://.+/webapi/v1/user/vehicles/{vin}/serviceExecutionStatus\?serviceType={service_type}'.format( vin=G31_VIN, service_type=service), data_files=[ _RESPONSE_PENDING, _RESPONSE_PENDING, _RESPONSE_DELIVERED, _RESPONSE_DELIVERED, _RESPONSE_EXECUTED]) with mock.patch('bimmer_connected.account.requests', new=backend_mock): account = ConnectedDriveAccount(TEST_USERNAME, TEST_PASSWORD, TEST_REGION) mock_listener = mock.Mock(return_value=None) account.add_update_listener(mock_listener) vehicle = account.get_vehicle(G31_VIN) response = getattr(vehicle.remote_services, call)() self.assertEqual(ExecutionState.EXECUTED, response.state) if triggers_update: mock_listener.assert_called_once_with() else: mock_listener.assert_not_called()
def get_status(args) -> None: """Get the vehicle status.""" account = ConnectedDriveAccount(args.username, args.password, get_region_from_name(args.region)) if args.lat and args.lng: for vehicle in account.vehicles: vehicle.set_observer_position(args.lat, args.lng) account.update_vehicle_states() print('Found {} vehicles: {}'.format( len(account.vehicles), ','.join([v.name for v in account.vehicles]))) for vehicle in account.vehicles: print('VIN: {}'.format(vehicle.vin)) print('mileage: {}'.format(vehicle.state.mileage)) print('vehicle properties:') print(json.dumps(vehicle.attributes, indent=4)) print('vehicle status:') print(json.dumps(vehicle.state.attributes, indent=4))
def test_get_remote_service_status(self): """Test get_remove_service_status method.""" backend_mock = BackendMock() with mock.patch('bimmer_connected.account.requests', new=backend_mock): account = ConnectedDriveAccount(TEST_USERNAME, TEST_PASSWORD, TEST_REGION) vehicle = account.get_vehicle(G31_VIN) with self.assertRaises(IOError): vehicle.remote_services._get_remote_service_status( remote_services._Services.REMOTE_LIGHT_FLASH) backend_mock.add_response( r'https://.+/webapi/v1/user/vehicles/{vin}/serviceExecutionStatus\?.+' .format(vin=G31_VIN), data_files=['G31_NBTevo/flash_executed.json']) status = vehicle.remote_services._get_remote_service_status( remote_services._Services.REMOTE_LIGHT_FLASH) self.assertEqual(ExecutionState.EXECUTED, status.state)
def __init__(self, username: str, password: str, region_str: str, name: str) -> None: """Constructor.""" from bimmer_connected.account import ConnectedDriveAccount from bimmer_connected.country_selector import get_region_from_name region = get_region_from_name(region_str) self.account = ConnectedDriveAccount(username, password, region) self.name = name self._update_listeners = []
def test_invalid_auth(self): """Test if the host is set correctly in the request.""" backend_mock = BackendMock() with mock.patch('bimmer_connected.account.requests', new=backend_mock): with mock.patch('bimmer_connected.account.get_gcdm_oauth_endpoint' ) as mocked_endpoint: mocked_endpoint( ).return_value = 'customer.bmwgroup.com/gcdm/invalid' with self.assertRaises(OSError): ConnectedDriveAccount(TEST_USERNAME, TEST_PASSWORD, Regions.REST_OF_WORLD)
def test_parsing_attributes(self): """Test parsing different attributes of the vehicle.""" backend_mock = BackendMock() with mock.patch('bimmer_connected.account.requests', new=backend_mock): account = ConnectedDriveAccount(TEST_USERNAME, TEST_PASSWORD, TEST_COUNTRY) for vehicle in account.vehicles: self.assertIsNotNone(vehicle.drive_train) self.assertIsNotNone(vehicle.name) self.assertIsNotNone(vehicle.has_rex)
class BMWConnectedDriveAccount: """Representation of a BMW vehicle.""" def __init__( self, username: str, password: str, region_str: str, name: str, read_only ) -> None: """Constructor.""" from bimmer_connected.account import ConnectedDriveAccount from bimmer_connected.country_selector import get_region_from_name region = get_region_from_name(region_str) self.read_only = read_only self.account = ConnectedDriveAccount(username, password, region) self.name = name self._update_listeners = [] def update(self, *_): """Update the state of all vehicles. Notify all listeners about the update. """ _LOGGER.debug( "Updating vehicle state for account %s, notifying %d listeners", self.name, len(self._update_listeners), ) try: self.account.update_vehicle_states() for listener in self._update_listeners: listener() except IOError as exception: _LOGGER.error( "Could not connect to the BMW Connected Drive portal. " "The vehicle state could not be updated." ) _LOGGER.exception(exception) def add_update_listener(self, listener): """Add a listener for update notifications.""" self._update_listeners.append(listener)
def test_available_attributes(self): """Check that available_attributes returns exactly the arguments we have in our test data.""" backend_mock = BackendMock() with mock.patch('bimmer_connected.account.requests', new=backend_mock): account = ConnectedDriveAccount(TEST_USERNAME, TEST_PASSWORD, TEST_REGION) for vin, dirname in TEST_VEHICLE_DATA.items(): vehicle = account.get_vehicle(vin) print(vehicle.name) status_data = load_response_json('{}/status.json'.format(dirname)) existing_attributes = status_data['vehicleStatus'].keys() existing_attributes = sorted([ ATTRIBUTE_MAPPING.get(a, a) for a in existing_attributes if a not in MISSING_ATTRIBUTES ]) expected_attributes = sorted([ a for a in vehicle.available_attributes if a not in ADDITIONAL_ATTRIBUTES ]) self.assertListEqual(existing_attributes, expected_attributes)
def test_drive_train_attributes(self): """Test parsing different attributes of the vehicle.""" backend_mock = BackendMock() with mock.patch('bimmer_connected.account.requests', new=backend_mock): account = ConnectedDriveAccount(TEST_USERNAME, TEST_PASSWORD, TEST_REGION) for vehicle in account.vehicles: print(vehicle.name) self.assertEqual(vehicle.vin in [G31_VIN, F48_VIN, F15_VIN, I01_VIN, F45_VIN], vehicle.has_internal_combustion_engine) self.assertEqual(vehicle.vin in [I01_VIN, I01_NOREX_VIN, F45_VIN], vehicle.has_hv_battery)
def test_charging_end_time(self): """Test if the parsing of mileage and range is working""" account = get_mocked_account() status = account.get_vehicle(VIN_G08).status self.assertEqual( datetime.datetime(2011, 11, 29, 4, 1, tzinfo=ConnectedDriveAccount.timezone()), status.charging_end_time)
def test_parsing_attributes(self): """Test parsing different attributes of the vehicle. Just make sure parsing that no exception is raised and we get not-None values. """ backend_mock = BackendMock() # list of attributes that are ignored at the moment ignored_attributes = [ ATTRIBUTE_MAPPING.get(a, a) for a in MISSING_ATTRIBUTES ] with mock.patch('bimmer_connected.account.requests', new=backend_mock): backend_mock.setup_default_vehicles() account = ConnectedDriveAccount(TEST_USERNAME, TEST_PASSWORD, TEST_REGION) account.update_vehicle_states() for vehicle in account.vehicles: print(vehicle.name) for attribute in (a for a in vehicle.available_attributes if a not in ignored_attributes): self.assertIsNotNone(getattr(vehicle.state, attribute), attribute)
def main(): """Store all responses from the backend in log files for later analysis.""" logging.basicConfig(level=logging.WARNING) print('Generating vehicle fingerprint...') parser = argparse.ArgumentParser() parser.add_argument('username') parser.add_argument('password') parser.add_argument('country') args = parser.parse_args() if os.path.exists(FINGERPRINT_DIR): shutil.rmtree(FINGERPRINT_DIR) os.mkdir(FINGERPRINT_DIR) account = ConnectedDriveAccount(args.username, args.password, args.country, log_responses=FINGERPRINT_DIR) account.update_vehicle_states() print('fingerprint of the vehicles written to {}'.format(FINGERPRINT_DIR))
def __init__( self, username: str, password: str, region_str: str, name: str, read_only: bool, lat=None, lon=None, ) -> None: """Initialize account.""" region = get_region_from_name(region_str) self.read_only = read_only self.account = ConnectedDriveAccount(username, password, region) self.name = name self._update_listeners = [] # Set observer position once for older cars to be in range for # GPS position (pre-7/2014, <2km) and get new data from API if lat and lon: self.account.set_observer_position(lat, lon) self.account.update_vehicle_states()
def test_parsing_attributes(self): """Test parsing different attributes of the vehicle.""" backend_mock = BackendMock() with mock.patch('bimmer_connected.account.requests', new=backend_mock): account = ConnectedDriveAccount(TEST_USERNAME, TEST_PASSWORD, TEST_REGION) for vehicle in account.vehicles: print(vehicle.name) self.assertIsNotNone(vehicle.drive_train) self.assertIsNotNone(vehicle.name) self.assertIsNotNone(vehicle.has_internal_combustion_engine) self.assertIsNotNone(vehicle.has_hv_battery) self.assertIsNotNone(vehicle.drive_train_attributes) self.assertIsNotNone(vehicle.statistics_available)
def main(): """Main function.""" logging.basicConfig(level=logging.DEBUG) parser = argparse.ArgumentParser() parser.add_argument('username') parser.add_argument('password') parser.add_argument('country') args = parser.parse_args() account = ConnectedDriveAccount(args.username, args.password, args.country) account.update_vehicle_states() print('Found {} vehicles: {}'.format( len(account.vehicles), ','.join([v.modelName for v in account.vehicles]))) for vehicle in account.vehicles: print('VIN: {}'.format(vehicle.vin)) print('mileage: {}'.format(vehicle.state.mileage)) print('Response from the server:') print(json.dumps(vehicle.state.attributes, indent=4)) print('vehicle specs:') print(json.dumps(vehicle.specs.attributes, indent=4))
def test_set_observer_some_none(self): """Test set_observer_position with invalid arguments.""" backend_mock = BackendMock() with mock.patch('bimmer_connected.account.requests', new=backend_mock): account = ConnectedDriveAccount(TEST_USERNAME, TEST_PASSWORD, Regions.REST_OF_WORLD) with self.assertRaises(ValueError): account.set_observer_position(None, 2.0) with self.assertRaises(ValueError): account.set_observer_position(1.0, None)
class BMWConnectedDriveAccount: """Representation of a BMW vehicle.""" def __init__( self, username: str, password: str, region_str: str, name: str, read_only: bool, lat=None, lon=None, ) -> None: """Initialize account.""" region = get_region_from_name(region_str) self.read_only = read_only self.account = ConnectedDriveAccount(username, password, region) self.name = name self._update_listeners = [] # Set observer position once for older cars to be in range for # GPS position (pre-7/2014, <2km) and get new data from API if lat and lon: self.account.set_observer_position(lat, lon) self.account.update_vehicle_states() def update(self, *_): """Update the state of all vehicles. Notify all listeners about the update. """ _LOGGER.debug( "Updating vehicle state for account %s, notifying %d listeners", self.name, len(self._update_listeners), ) try: self.account.update_vehicle_states() for listener in self._update_listeners: listener() except OSError as exception: _LOGGER.error( "Could not connect to the BMW Connected Drive portal. " "The vehicle state could not be updated" ) _LOGGER.exception(exception) def add_update_listener(self, listener): """Add a listener for update notifications.""" self._update_listeners.append(listener)
def fingerprint(args) -> None: """Save the vehicle fingerprint.""" time_dir = Path.home() / 'vehicle_fingerprint' / time.strftime("%Y-%m-%d_%H-%M-%S") time_dir.mkdir(parents=True) account = ConnectedDriveAccount(args.username, args.password, get_region_from_name(args.region), log_responses=time_dir) account.set_observer_position(args.lat, args.lng) if args.lat and args.lng: for vehicle in account.vehicles: vehicle.set_observer_position(args.lat, args.lng) account.update_vehicle_states() print('fingerprint of the vehicles written to {}'.format(time_dir))
def fingerprint(args) -> None: """Save the vehicle fingerprint.""" time_str = time.strftime("%Y-%m-%d_%H-%M-%S") time_dir = os.path.join(FINGERPRINT_DIR, time_str) os.makedirs(time_dir) account = ConnectedDriveAccount(args.username, args.password, get_region_from_name(args.region), log_responses=time_dir) account.set_observer_position(args.lat, args.lng) if args.lat and args.lng: for vehicle in account.vehicles: vehicle.set_observer_position(args.lat, args.lng) account.update_vehicle_states() print('fingerprint of the vehicles written to {}'.format(time_dir))
def fingerprint(args) -> None: """Save the vehicle fingerprint.""" time_dir = Path.home() / 'vehicle_fingerprint' / time.strftime( "%Y-%m-%d_%H-%M-%S") time_dir.mkdir(parents=True) account = ConnectedDriveAccount(args.username, args.password, get_region_from_name(args.region), log_responses=time_dir) if args.lat and args.lng: for vehicle in account.vehicles: vehicle.set_observer_position(args.lat, args.lng) # doesn't work anymore # account.update_vehicle_states() # Patching in new My BMW endpoints for fingerprinting server_url = get_server_url(get_region_from_name(args.region)) for vehicle in account.vehicles: if vehicle.drive_train in HV_BATTERY_DRIVE_TRAINS: print("Getting 'charging-sessions' for {}".format(vehicle.vin)) account.send_request( "https://{}/eadrax-chs/v1/charging-sessions".format( server_url), params={ "vin": vehicle.vin, "maxResults": 40, "include_date_picker": "true" }, logfilename="charging-sessions") print("Getting 'charging-statistics' for {}".format(vehicle.vin)) account.send_request( "https://{}/eadrax-chs/v1/charging-statistics".format( server_url), params={ "vin": vehicle.vin, "currentDate": datetime.utcnow().isoformat() }, logfilename="charging-statistics") print('fingerprint of the vehicles written to {}'.format(time_dir))
def test_anonymize_data(self): """Test anonymization function.""" test_dict = { 'vin': 'secret', 'a sub-dict': { 'lat': 666, 'lon': 666, 'heading': 666, }, 'licensePlate': 'secret', 'public': 'public_data', 'a_list': [ {'vin': 'secret'}, { 'lon': 666, 'public': 'more_public_data', }, ] } anon_text = json.dumps(ConnectedDriveAccount._anonymize_data(test_dict)) self.assertNotIn('secret', anon_text) self.assertNotIn('666', anon_text) self.assertIn('public_data', anon_text) self.assertIn('more_public_data', anon_text)
def __init__(self, config, vehicles, logger): self.vehicles = [ vehicle for vehicle in vehicles if vehicle["manufacturer"] == "bmw" ] self.config = config self.logger = logger self.logger.info("BMW Connected Drive Cache started...") self.cache = get_cache_dir() self.max_staleness = self.config["max_staleness"] # minutes credentials = self.config["credentials"] region = get_region_from_name(credentials["region"]) self.account = ConnectedDriveAccount(credentials["user"], credentials["pwd"], region) self.vehicle_aliases: Dict[str, str] = { vehicle["vin"]: vehicle["alias"] for vehicle in self.vehicles } self.vehicle_update_timestamps: Dict[str, float] = { vehicle["vin"]: 0. for vehicle in self.vehicles }
def setupThing(info): # Setup for the account if info.thing.thingClassId == accountThingClassId: logger.log("SetupThing for account:", info.thing.name) region = regions[info.thing.paramValue(accountThingRegionParamTypeId)] pluginStorage().beginGroup(info.thing.id) username = pluginStorage().value("username") password = pluginStorage().value("password") pluginStorage().endGroup() try: account = ConnectedDriveAccount(username, password, region) account.update_vehicle_states() accountsMap[info.thing.id] = account except Exception as e: # Login error logger.warn(f"Error setting up BMW account: {str(e)}") info.finish(nymea.ThingErrorAuthenticationFailure, str(e)) return # Mark the account as logged-in and connected info.thing.setStateValue(accountLoggedInStateTypeId, True) info.thing.setStateValue(accountConnectedStateTypeId, True) # Login went well, finish the setup info.finish(nymea.ThingErrorNoError) logger.log( f"Found {len(account.vehicles)} vehicles: {', '.join([v.name for v in account.vehicles])}" ) thingDescriptors = [] for vehicle in account.vehicles: if any(thing.thingClassId == vehicleThingClassId and thing.paramValue(vehicleThingVinParamTypeId) == vehicle.vin for thing in myThings()): continue if not vehicle.has_hv_battery: logger.log( f"Ignoring combustion vehicle {vehicle.name} ({vehicle.vin[-7:]})" ) continue logger.log( f"Adding new vehicle {vehicle.name} ({vehicle.vin[-7:]}) to the system with parent {info.thing.id}" ) thingDescriptor = nymea.ThingDescriptor( vehicleThingClassId, "BMW {} ({})".format(vehicle.name, vehicle.vin[-7:]), parentId=info.thing.id, ) thingDescriptor.params = [ nymea.Param(vehicleThingVinParamTypeId, vehicle.vin) ] thingDescriptors.append(thingDescriptor) autoThingsAppeared(thingDescriptors) # If no poll timer is set up yet, start it now logger.log("Creating polltimer @ setupThing") global pollTimer if pollTimer is None: pollTimer = nymea.PluginTimer(60 * 5, pollService) logger.log("timer interval @ setupThing", pollTimer.interval) # Setup for the vehicles if info.thing.thingClassId == vehicleThingClassId: info.finish(nymea.ThingErrorNoError)
# Your car's full VIN, if you have more then one. You can leave it empty otherwise. BMW_CONNECTED_DRIVE_VIN = "" LOGO = "iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAMAAAAoLQ9TAAAABGdBTUEAALGPC/xhBQAAACBjSFJNAAB6JgAAgIQAAPoAAACA6AAAdTAAAOpgAAA6mAAAF3CculE8AAABX1BMVEV0dHT///9ubm5vb29JbYdwcHALY6QrapgqZpYAZrGmkH6cinssapgkZZk3a5GXl5cfaZjkoGtlb3ZxcXFycnJwcHBvb29wcHBwcHBwcHBwcHBwcHBwcHBxcXFwcHBwcHBvb29wcHBxcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHBycnF1cW5rcHRvcHBxcXBwcHBwcHBwcHBzc3NwcHBwcHBxcHAUaKYEZ68IZ61TboJzcW9wcHBwcHBwcHBwcHBwcHBxcHA0a5MBZrEAZrEMZ6p+cWdxcXFxcXFvb28sa5kCZrAAZrEAZrJ+fn5ycnJxcXFwcHBycG8AZrMAZrEAZrF+cWdzcG9wcHBwcHC1h2QVaKcEZ68AZrEAZrEAZrEOZ6lQbYNkbneLg360hmRxcXFzcW8AZ7JxcXFycnJ7e3sAZrEAZ7Jvb3CAgIBzc3MAZ7JwcHBwcHBwcHAAZrH////TSxzlAAAAc3RSTlMAAAAAAAAAAAAAAAAAAAAAAAAAAAlCc3+FhnxzQRllWyJAkZUxIl1pfXkcL3Z+JzpkcAmPkGwxfpxIOSuRiFxGkInU5iAON2IkQdfjGwI3KncLeOUaDzQLdQEHl+fNMiBBCwEBdDV5HTQC0ygkAStJZzdyTLX+NwAAAAFiS0dEAf8CLd4AAAAHdElNRQflAwEXLBPRp6KhAAABB0lEQVQY0xXPh1qCUBgG4B+MzNwFNLEt7WG2i7ZRUB0CAgFnOXIUeO7/eTpewDdeAIpmOX5qemZ2bp6lKQAqJGQWFpeWV1bXsqIQooAW1jc2R7a2d3aZvf0DgYaceJg/Oj45PTu/uLzKiDngpOub2zt8//BYeMoXshzw8ujzC8aKGn59e2dkHpA29oEx1o1I2PwctxDYRcf1FL1UNtRKtVa3wa7XqhXVKJd0xXOdr29AVrRhxiKGTnLNuIZIKdNq/8RUBeNON9HjyWyy/9s2Kx7u/KXSEgesHzD9VqPqNrupicBnyfVBkGSiNSeeSAcDcp3gfEm26kWtJ/kCM+QO+ci2Ec8R/uQ/WgA6IA6KdS0AAAAldEVYdGRhdGU6Y3JlYXRlADIwMjEtMDMtMDFUMjM6NDE6MDIrMDA6MDCCqrT8AAAAJXRFWHRkYXRlOm1vZGlmeQAyMDIxLTAyLTI4VDExOjE0OjI3KzAwOjAwtgzNTQAAAABJRU5ErkJggg==" from bimmer_connected.account import ConnectedDriveAccount from bimmer_connected.country_selector import get_region_from_name from bimmer_connected.vehicle import VehicleViewDirection from bimmer_connected.state import ChargingState import base64 import datetime account = ConnectedDriveAccount( BMW_CONNECTED_DRIVE_USERNAME, BMW_CONNECTED_DRIVE_PASSWORD, get_region_from_name(BMW_CONNECTED_DRIVE_REGION), ) account.update_vehicle_states() if not BMW_CONNECTED_DRIVE_VIN: account_vins = [] for vehicle in account.vehicles: account_vins.append(vehicle.vin) # we'll just pick the first one BMW_CONNECTED_DRIVE_VIN = account_vins[0] vehicle = account.get_vehicle(BMW_CONNECTED_DRIVE_VIN) image_data = vehicle.get_vehicle_image(200, 80, VehicleViewDirection.FRONT) b64image = base64.b64encode(image_data)