├── .gitignore ├── README.md ├── Run.py ├── audiapi ├── API.py ├── Services.py ├── Token.py ├── __init__.py └── model │ ├── BatteryChargeResponse.py │ ├── ClimaRequest.py │ ├── CurrentVehicleDataResponse.py │ ├── HonkFlash.py │ ├── OperationsID.py │ ├── PinAuth.py │ ├── RequestStatus.py │ ├── ServiceID.py │ ├── Vehicle.py │ ├── VehicleDataResponse.py │ └── __init__.py ├── credentials.template.json ├── publish.sh └── setup.py /.gitignore: -------------------------------------------------------------------------------- 1 | .idea/* 2 | *.iml 3 | **/__pycache__ 4 | token.json 5 | credentials.json 6 | /build 7 | /audiapi.egg-info 8 | /dist 9 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Audi Connect API 2 | This library provides access to the Audi Connect API. 3 | It allows easy access to all vehicle relevant data. 4 | 5 | The API does automatic session caching and tries to keep the 6 | original service names as found in the audi connect app. 7 | 8 | Some services require special permissions or certain origin countries of the car 9 | to be accessible. 10 | 11 | Note: Not all services are fully implemented due to missing permissions as mentioned above. 12 | 13 | # Installing 14 | ```bash 15 | pip install audiapi 16 | ``` 17 | 18 | # Examples 19 | ## Login credentials 20 | `credentials.json` 21 | ```json 22 | { 23 | "user": "yourUser@mail.com", 24 | "pass": "yourPassword" 25 | } 26 | ``` 27 | 28 | ## List all vehicles under your account 29 | The auth tokens will be cached in the working directory 30 | ```python 31 | api = API() 32 | logon_service = LogonService(api) 33 | if not logon_service.restore_token(): 34 | with open('credentials.json') as data_file: 35 | data = json.load(data_file) 36 | logon_service.login(data['user'], data['pass']) 37 | car_service = CarService(api) 38 | car_service.get_vehicles() 39 | ``` 40 | Response 41 | ```json 42 | { 43 | "csid": "-----", 44 | "vin": "-----", 45 | "registered": "2017-02-15T18:06:39.000+01:00" 46 | } 47 | ``` 48 | 49 | ## Get details about the embedded SIM 50 | ```python 51 | mgmt_service = VehicleManagementService(api, vehicle) 52 | mgmt_service.get_information() 53 | ``` 54 | Response 55 | ```json 56 | { 57 | "vehicleData": { 58 | "requestId": "--", 59 | "vin": "--", 60 | "country": "DE", 61 | "isConnect": "true", 62 | "brand": "Audi", 63 | "vehicleDevices": { 64 | "vehicleDevice": [ 65 | { 66 | "ecuGeneration": "MIB2high", 67 | "deviceType": "INFOTAINMENT" 68 | }, 69 | { 70 | "deviceId": "----", 71 | "embeddedSim": { 72 | "identification": { 73 | "content": "----", 74 | "type": "ICCID" 75 | }, 76 | "imei": "----", 77 | "mno": "vodafone" 78 | }, 79 | "ecuGeneration": "cGW", 80 | "deviceType": "TELEMATICS" 81 | } 82 | ] 83 | }, 84 | "isConnectSorglosReady": "true", 85 | "systemId": "XID_APP_AUDI" 86 | } 87 | } 88 | ``` 89 | 90 | # Dependencies 91 | - Python 3 92 | - Requests library -------------------------------------------------------------------------------- /Run.py: -------------------------------------------------------------------------------- 1 | import json 2 | 3 | from audiapi.API import API 4 | from audiapi.Services import LogonService, CarService 5 | 6 | 7 | def main(): 8 | api = API() 9 | logon_service = LogonService(api) 10 | if not logon_service.restore_token(): 11 | # We need to login 12 | with open('credentials.json') as data_file: 13 | data = json.load(data_file) 14 | logon_service.login(data['user'], data['pass']) 15 | 16 | car_service = CarService(api) 17 | vehicles_response = car_service.get_vehicles() 18 | for vehicle in vehicles_response.vehicles: 19 | print(str(vehicle)) 20 | 21 | 22 | if __name__ == '__main__': 23 | main() 24 | -------------------------------------------------------------------------------- /audiapi/API.py: -------------------------------------------------------------------------------- 1 | import json 2 | 3 | import requests 4 | 5 | from audiapi.Token import Token 6 | 7 | 8 | class API: 9 | """ 10 | Wrapper for the audi API 11 | """ 12 | BASE_URL = 'https://msg.audi.de/fs-car' 13 | BASE_CAR_URL = 'https://msg.audi.de/fs-car/bs/cf/v1/Audi/DE/vehicles' 14 | 15 | BASE_HEADERS = {'Accept': 'application/json', 16 | 'X-App-ID': 'de.audi.mmiapp', 17 | 'X-App-Name': 'MMIconnect', 18 | 'X-App-Version': '2.8.3', 19 | 'X-Brand': 'audi', 20 | 'X-Country-Id': 'DE', 21 | 'X-Language-Id': 'de', 22 | 'X-Platform': 'google', 23 | 'User-Agent': 'okhttp/2.7.4', 24 | 'ADRUM_1': 'isModule:true', 25 | 'ADRUM': 'isAray:true'} 26 | 27 | def __init__(self, proxy=None): 28 | """ 29 | Creates a new API 30 | 31 | :param proxy: Proxy which should be used in the URL format e.g. http://proxy:8080 32 | """ 33 | self.__token = None 34 | if proxy is not None: 35 | self.__proxy = {'http': proxy, 36 | 'https': proxy} 37 | else: 38 | self.__proxy = None 39 | 40 | def use_token(self, token: Token): 41 | """ 42 | Uses the given token for auth 43 | 44 | :param token: Token 45 | """ 46 | self.__token = token 47 | 48 | def get(self, url): 49 | r = requests.get(url, headers=self.__get_headers(), proxies=self.__proxy) 50 | return self.__handle_error(r.json()) 51 | 52 | def put(self, url, data=None, headers=None): 53 | full_headers = self.__get_headers() 54 | full_headers.update(headers) 55 | r = requests.put(url, data, headers=full_headers, proxies=self.__proxy) 56 | return self.__handle_error(r.json()) 57 | 58 | def post(self, url, data=None, use_json: bool = True): 59 | if use_json and data is not None: 60 | data = json.dumps(data) 61 | r = requests.post(url, data=data, headers=self.__get_headers(), proxies=self.__proxy) 62 | return self.__handle_error(r.json()) 63 | 64 | def __handle_error(self, data): 65 | error = data.get('error') 66 | if error is not None: 67 | error_msg = data.get('error_description', '') 68 | raise Exception('API error: ' + str(error) + '\n' + error_msg) 69 | return data 70 | 71 | def __get_headers(self): 72 | full_headers = dict() 73 | full_headers.update(self.BASE_HEADERS) 74 | token_value = 'AudiAuth 1' 75 | if self.__token is not None: 76 | token_value += ' ' + self.__token.access_token 77 | full_headers['Authorization'] = token_value 78 | return full_headers 79 | -------------------------------------------------------------------------------- /audiapi/Services.py: -------------------------------------------------------------------------------- 1 | from abc import abstractmethod, ABCMeta 2 | 3 | from audiapi.API import Token, API 4 | from audiapi.model.ClimaRequest import ClimaRequestFactory 5 | from audiapi.model.CurrentVehicleDataResponse import CurrentVehicleDataResponse 6 | from audiapi.model.HonkFlash import HonkFlashAction, RemoteHonkFlashActionStatus 7 | from audiapi.model.RequestStatus import RequestStatus 8 | from audiapi.model.Vehicle import VehiclesResponse, Vehicle 9 | from audiapi.model.VehicleDataResponse import VehicleDataResponse 10 | from audiapi.model.BatteryChargeResponse import BatteryChargeResponse 11 | 12 | 13 | class Service(metaclass=ABCMeta): 14 | BASE_URL = 'https://msg.audi.de/fs-car' 15 | COMPANY = 'Audi' 16 | COUNTRY = 'DE' 17 | 18 | def __init__(self, api: API): 19 | self._api = api 20 | """ 21 | API for communicating 22 | 23 | :type _api: API 24 | """ 25 | 26 | def url(self, part, **format_data): 27 | """ 28 | Builds a full URL using the given parts 29 | 30 | :param part URL part which should be added at the end 31 | :param format_data: Format arguments 32 | :return: URL 33 | :rtype: str 34 | """ 35 | url = self.BASE_URL + '/' + self._get_path() 36 | if self._use_company(): 37 | url += '/' + self.COMPANY + '/' + self.COUNTRY 38 | url += part 39 | return url.format(**format_data) 40 | 41 | def _use_company(self): 42 | return True 43 | 44 | @abstractmethod 45 | def _get_path(self): 46 | """ 47 | Returns the url path for this service 48 | 49 | :return: URL path 50 | :rtype: str 51 | """ 52 | pass 53 | 54 | 55 | class VehicleService(Service, metaclass=ABCMeta): 56 | def __init__(self, api: API, vehicle: Vehicle): 57 | super().__init__(api) 58 | self._vehicle = vehicle 59 | """ 60 | Current vehicle 61 | 62 | :type _vehicle: Vehicle 63 | """ 64 | 65 | def url(self, part, **format_data): 66 | return super().url(part, **format_data, vin=self._vehicle.vin) 67 | 68 | 69 | class AuthorizationService(Service): 70 | """ 71 | Auth flow (?): 72 | --service, operation--> 73 | <--PinAuthInfoResponse-- 74 | -- PinChallenge --> (?) 75 | .... 76 | 77 | Complete: 78 | PinChallenge+data -- CompleteAuthenticationRequest --> 79 | """ 80 | 81 | def request_auth(self, vehicle: Vehicle, service: str, operation: str): 82 | headers = {'Content-Length': '0', 'Content-Type': 'application/json; charset=UTF-8'} 83 | url = self.url('/vehicles/{vin}/services/{service}/operations/{operation}/request', 84 | vin=vehicle.vin, service=service, operation=operation) 85 | 86 | data = {} # Yes send empty data - no idea why 87 | return self._api.put(url, data=data, headers=headers) 88 | 89 | def complete_auth(self): 90 | """ 91 | Completes the auth request 92 | """ 93 | data = {} 94 | return self._api.post(self.url('/complete'), data) 95 | 96 | def _get_path(self): 97 | return 'rolesrights/authorization/v1' 98 | 99 | 100 | class CarFinderService(VehicleService): 101 | """ 102 | Requires special permissions - might be for rental car companies 103 | """ 104 | 105 | def find(self): 106 | """ 107 | Returns the position of the car 108 | """ 109 | return self._api.get(self.url('/vehicles/{vin}/position')) 110 | 111 | def _get_path(self): 112 | return 'bs/cf/v1' 113 | 114 | 115 | class CarService(Service): 116 | def get_vehicles(self): 117 | """ 118 | Returns all cars registered for the current account 119 | 120 | :return: VehiclesResponse 121 | :rtype: VehiclesResponse 122 | """ 123 | 124 | data = self._api.get(self.url('/vehicles')) 125 | response = VehiclesResponse() 126 | response.parse(data) 127 | return response 128 | 129 | def get_vehicle_data(self, vehicle: Vehicle): 130 | """ 131 | Returns the vehicle data for the given vehicle 132 | 133 | :param vehicle: Vehicle with CSID 134 | :return: Vehicle data 135 | """ 136 | return self._api.get(self.url('/vehicle/{csid}'.format(csid=vehicle.csid))) 137 | 138 | def _get_path(self): 139 | return 'myaudi/carservice/v2' 140 | 141 | 142 | class ClimateService(Service): 143 | def _get_path(self): 144 | return 'bs/rs/v1' 145 | 146 | 147 | class DiebstahlwarnanlageService(Service): 148 | def _get_path(self): 149 | return 'bs/dwap/v1' 150 | 151 | 152 | class GeofenceService(Service): 153 | """ 154 | USA only - Restrict car area 155 | """ 156 | 157 | def _get_path(self): 158 | return 'bs/geofencing/v1' 159 | 160 | 161 | class LockUnlockService(VehicleService): 162 | """ 163 | Locks and unlocks the car 164 | """ 165 | 166 | def get_actions(self): 167 | """ 168 | Returns all available actions 169 | """ 170 | return self._api.get(self.url("/vehicles/{vin}/actions")) 171 | 172 | # TODO: Lock and unlock request 173 | 174 | def _get_path(self): 175 | return 'bs/rlu/v1' 176 | 177 | 178 | class LogonService(Service): 179 | """ 180 | General API logon service 181 | """ 182 | 183 | def login(self, user: str, password: str, persist_token: bool = True): 184 | """ 185 | Creates a new session using the given credentials 186 | 187 | :param user: User 188 | :param password: Password 189 | :param persist_token: True if the token should be persisted in the file system after login 190 | """ 191 | token = self.__login_request(user, password) 192 | self._api.use_token(token) 193 | if persist_token: 194 | token.persist() 195 | 196 | def restore_token(self): 197 | """ 198 | Tries to restore the latest persisted auth token 199 | 200 | :return: True if token could be restored 201 | :rtype: bool 202 | """ 203 | token = Token.load() 204 | if token is None or not token.valid(): 205 | return False 206 | self._api.use_token(token) 207 | return True 208 | 209 | def __login_request(self, user: str, password: str): 210 | """ 211 | Requests a login token for the given user 212 | 213 | :param user: User 214 | :param password: Password 215 | :return: Token 216 | :rtype: Token 217 | """ 218 | data = {'grant_type': 'password', 219 | 'username': user, 220 | 'password': password} 221 | reply = self._api.post(self.url('/token'), data, use_json=False) 222 | return Token.parse(reply) 223 | 224 | def _get_path(self): 225 | return 'core/auth/v1' 226 | 227 | 228 | class MobileKeyService(Service): 229 | """ 230 | Manages keyless access for the car 231 | """ 232 | 233 | def _get_path(self): 234 | return '// Not implemented in MMI app' 235 | 236 | 237 | class OperationListService(VehicleService): 238 | """ 239 | Provides access to all permissions one can set for telemetrics and MMI (connect). 240 | This will also tell you how long your licences are valid, 241 | and when the service reaches it's final EOL date 242 | """ 243 | 244 | def get_operations(self): 245 | """ 246 | Returns all services available and their license status 247 | """ 248 | return self._api.get(self.url('/vehicles/{vin}/operations')) 249 | 250 | def _get_path(self): 251 | return 'rolesrights/operationlist/v2' 252 | 253 | 254 | class PictureNavigationService(VehicleService): 255 | def get_all(self): 256 | # Returns 404 for some reason - might need to wireshark the correct path 257 | return self._api.get(self.url('/vehicles/{vin}/all')) 258 | 259 | def _get_path(self): 260 | return 'audi/b2c/picturenav/v1' 261 | 262 | 263 | class PoiNavigationService(Service): 264 | def _get_path(self): 265 | return 'audi/b2c/poinav/v1' 266 | 267 | 268 | class OnlineDestinationsService(VehicleService): 269 | def get_pois(self): 270 | return self._api.get(self.url('/vehicles/{vin}/pois')) 271 | 272 | def _get_path(self): 273 | return '' # TODO 274 | 275 | 276 | class PreTripClimaService(VehicleService): 277 | """ 278 | Access to clima control - (EV only?) 279 | """ 280 | 281 | def get_status(self): 282 | return self._api.get(self.url('/vehicles/{vin}/climater')) 283 | 284 | def get_request_status(self, action_id: str): 285 | return self._api.get(self.url('/vehicles/{vin}/climater/actions/{action_id}', action_id=action_id)) 286 | 287 | def perform_action(self, request_factory: ClimaRequestFactory): 288 | self._api.post(self.url('/vehicles/{vin}/climater/actions'), data=request_factory.build()) 289 | 290 | def _get_path(self): 291 | return 'bs/climatisation/v1' 292 | 293 | 294 | class PushNotificationService(Service): 295 | """ 296 | Registers push notifications (of some sort) 297 | """ 298 | 299 | PLATFORM_GOOGLE = 'google' 300 | APP_ID = 'de.audi.mmiapp' 301 | 302 | def register(self, platform: str, app_id: str, token: str): 303 | """ 304 | Registers a push notification service 305 | :param platform: Platform 306 | :param app_id: App ID 307 | :param token: Google messaging service token 308 | :return: 309 | """ 310 | self._api.post(self.url('/subscriptions/{platform}/{app_id}/{token}', platform=platform, app_id=app_id, 311 | token=token), data={}) 312 | 313 | def _get_path(self): 314 | return 'fns/subscription/v1' 315 | 316 | 317 | class RemoteBatteryChargeService(VehicleService): 318 | """ 319 | For EV only - battery status and charge management 320 | """ 321 | 322 | def get_status(self): 323 | """ 324 | Returns battery charge status 325 | 326 | :return: BatteryChargeResponse 327 | :rtype: BatteryChargeResponse 328 | """ 329 | 330 | data = self._api.get(self.url('/vehicles/{vin}/charger')) 331 | response = BatteryChargeResponse() 332 | response.parse(data) 333 | return response 334 | 335 | def _get_path(self): 336 | return 'bs/batterycharge/v1' 337 | 338 | 339 | class RemoteDepartureTimeService(Service): 340 | """ 341 | For EV only - timer for choosing when the battery should be fully charged 342 | """ 343 | 344 | def _get_path(self): 345 | return 'bs/departuretimer/v1' 346 | 347 | 348 | class RemoteHonkFlashService(VehicleService): 349 | """ 350 | Note: This service requires special auth from vw/audi :( 351 | """ 352 | 353 | FLASH_ONLY = "FLASH_ONLY" 354 | HONK_AND_FLASH = "HONK_AND_FLASH" 355 | HONK_ONLY = "HONK_ONLY" 356 | 357 | def flash(self, seconds: int): 358 | """ 359 | Flashes the car for the given amount 360 | 361 | :param seconds: Seconds 362 | :return: HonkFlashAction 363 | :rtype: HonkFlashAction 364 | """ 365 | data = {'honkAndFlashRequest': { 366 | 'userPosition': {'latitude': 0, 367 | 'longitude': 0 368 | }, 369 | 'serviceDuration': seconds, 370 | 'serviceOperationCode': self.FLASH_ONLY 371 | }} 372 | data = self._api.post(self.url('/vehicles/{vin}/honkAndFlash'), 373 | data=data) 374 | return HonkFlashAction(data) 375 | 376 | def get_status(self, action: HonkFlashAction): 377 | """ 378 | Returns the status of the given action 379 | 380 | :param action: Action 381 | :return: RemoteHonkFlashActionStatus 382 | :rtype: RemoteHonkFlashActionStatus 383 | """ 384 | data = self._api.get(self.url('/vehicles/{vin}/honkAndFlash/{action_id}/status', action_id=action.id)) 385 | return RemoteHonkFlashActionStatus(data) 386 | 387 | def _get_path(self): 388 | return 'bs/rhf/v1' 389 | 390 | 391 | class RemoteTripStatisticsService(VehicleService): 392 | """ 393 | Trip statistics (like range, fuel consumption, ...) 394 | """ 395 | LONG_TERM = 'longTerm' 396 | SHORT_TERM = 'shortTerm' 397 | 398 | def get_latest(self, trip_type: str): 399 | """ 400 | Returns the latest trip statistic 401 | """ 402 | return self._api.get(self.url('/vehicles/{vin}/tripdata/{trip_type}?newest', trip_type=trip_type)) 403 | 404 | def _get_path(self): 405 | return 'bs/tripstatistics/v1' 406 | 407 | 408 | class SpeedAlertService(VehicleService): 409 | """ 410 | USA only - monitors if the car has been driven too fast 411 | """ 412 | 413 | def get_list(self): 414 | return self._api.get(self.url('/vehicles/{vin}/speedAlerts')) 415 | 416 | def _get_path(self): 417 | return 'bs/speedalert/v1' 418 | 419 | 420 | class UserInfoService(Service): 421 | """ 422 | General user information 423 | """ 424 | 425 | def get_info(self): 426 | return self._api.get(self.url('/userInfo')) 427 | 428 | def _get_path(self): 429 | return 'core/auth/v1' 430 | 431 | 432 | class UserManagementService(VehicleService): 433 | """ 434 | Manages car pairing stuff 435 | """ 436 | 437 | def get_paring_status(self): 438 | return self._api.get(self.url('/vehicles/{vin}/pairing')) 439 | 440 | def _get_path(self): 441 | return 'usermanagement/users/v1' 442 | 443 | 444 | class ValetAlertService(Service): 445 | """ 446 | USA only - Alerting for invalid car usage 447 | """ 448 | 449 | def get_alerts(self): 450 | return self._api.get(self.url('/vehicles/{vin}/valetAlerts')) 451 | 452 | def get_definition(self): 453 | return self._api.get(self.url('/vehicles/{vin}/valetAlertDefinition')) 454 | 455 | def get_request_status(self, request_id: str): 456 | return self._api.get(self.url('/vehicles/{vin}/valetAlertDefinition/{id}/status', id=request_id)) 457 | 458 | def set_definition(self, definition): 459 | # TODO: Implement definition 460 | return self._api.post(self.url('/vehicles/{vin}/valetAlertDefinition', data={})) 461 | 462 | def _get_path(self): 463 | return 'bs/valetalert/v1' 464 | 465 | 466 | class VehicleManagementService(VehicleService): 467 | """ 468 | Information about the vehicle management system 469 | """ 470 | 471 | def get_information(self): 472 | """ 473 | Returns information about the connection system of the vehicle 474 | (such as embedded sim) 475 | """ 476 | return self._api.get(self.url('/vehicles/{vin}')) 477 | 478 | def _get_path(self): 479 | return 'vehicleMgmt/vehicledata/v2' 480 | 481 | 482 | class VehicleStatusReportService(VehicleService): 483 | """ 484 | General status of the vehicle 485 | """ 486 | 487 | def get_request_status(self, request_id: str): 488 | """ 489 | Returns the status of the request with the given ID 490 | 491 | :param request_id: Request ID 492 | :return: RequestStatus 493 | :rtype: RequestStatus 494 | """ 495 | data = self._api.get(self.url('/vehicles/{vin}/requests/{request_id}/jobstatus', request_id=request_id)) 496 | return RequestStatus(data) 497 | 498 | def get_requested_current_vehicle_data(self, request_id: str): 499 | """ 500 | Returns the vehicle report of the request with the given ID 501 | 502 | :param request_id: Request ID 503 | :return: VehicleDataResponse 504 | :rtype: VehicleDataResponse 505 | """ 506 | data = self._api.get(self.url('/vehicles/{vin}/requests/{request_id}/status', request_id=request_id)) 507 | return VehicleDataResponse(data) 508 | 509 | def request_current_vehicle_data(self): 510 | """ 511 | Requests the latest report data from the vehicle 512 | 513 | :return: CurrentVehicleDataResponse 514 | :rtype: CurrentVehicleDataResponse 515 | """ 516 | data = self._api.post(self.url('/vehicles/{vin}/requests')) 517 | return CurrentVehicleDataResponse(data) 518 | 519 | def get_stored_vehicle_data(self): 520 | """ 521 | Returns the last vehicle data received 522 | 523 | :return: VehicleDataResponse 524 | :rtype: VehicleDataResponse 525 | """ 526 | data = self._api.get(self.url('/vehicles/{vin}/status')) 527 | return VehicleDataResponse(data) 528 | 529 | def _get_path(self): 530 | return 'bs/vsr/v1' 531 | -------------------------------------------------------------------------------- /audiapi/Token.py: -------------------------------------------------------------------------------- 1 | import json 2 | import os 3 | 4 | import time 5 | 6 | 7 | class Token: 8 | FILE = 'token.json' 9 | 10 | def __init__(self): 11 | self.access_token = '' 12 | self.token_type = '' 13 | self.expires_in = 0 14 | 15 | def valid(self): 16 | """ 17 | Checks if this token is still valid 18 | 19 | :return: True if valid 20 | :rtype: bool 21 | """ 22 | return self.expires_in > int(time.time()) 23 | 24 | def persist(self): 25 | with open(self.FILE, 'w') as outfile: 26 | json.dump(self.__dict__, outfile) 27 | 28 | @staticmethod 29 | def parse(data, relative_timestamp=True): 30 | token = Token() 31 | token.access_token = data.get('access_token') 32 | token.token_type = data.get('token_type') 33 | raw_timestamp = data.get('expires_in') 34 | if relative_timestamp: 35 | raw_timestamp += int(time.time()) 36 | token.expires_in = raw_timestamp 37 | return token 38 | 39 | def __str__(self): 40 | return 'Access token: ' + self.access_token 41 | 42 | @staticmethod 43 | def load(): 44 | if not os.path.isfile(Token.FILE): 45 | return None 46 | 47 | with open(Token.FILE) as data_file: 48 | data = json.load(data_file) 49 | return Token.parse(data, relative_timestamp=False) 50 | -------------------------------------------------------------------------------- /audiapi/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/davidgiga1993/AudiAPI/f3f629c45c484c25eb75ffebca03b743e09a1cdc/audiapi/__init__.py -------------------------------------------------------------------------------- /audiapi/model/BatteryChargeResponse.py: -------------------------------------------------------------------------------- 1 | class BatteryChargeResponse: 2 | """ 3 | Represents a battery charge response 4 | """ 5 | 6 | def parse(self, data): 7 | self.charger = data.get('charger') 8 | 9 | def __str__(self): 10 | return str(self.__dict__) 11 | -------------------------------------------------------------------------------- /audiapi/model/ClimaRequest.py: -------------------------------------------------------------------------------- 1 | from abc import abstractmethod 2 | 3 | 4 | class ClimaRequestFactory: 5 | @abstractmethod 6 | def build(self): 7 | pass 8 | 9 | 10 | class StartClimaRequestFactory(ClimaRequestFactory): 11 | SOURCE_AUX = 'auxiliary' 12 | SOURCE_AUTOMATIC = 'automatic' 13 | SOURCE_ELECTRIC = 'electric' 14 | 15 | def __init__(self, source: str): 16 | self.__source = source 17 | 18 | def build(self): 19 | return { 20 | 'action': { 21 | 'type': 'startClimatisation', 22 | 'settings': { 23 | 'heaterSource': self.__source 24 | } 25 | } 26 | } 27 | 28 | 29 | class StopClimaRequestFactory(ClimaRequestFactory): 30 | def build(self): 31 | return { 32 | 'action': { 33 | 'type': 'stopClimatisation' 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /audiapi/model/CurrentVehicleDataResponse.py: -------------------------------------------------------------------------------- 1 | class CurrentVehicleDataResponse: 2 | def __init__(self, data): 3 | data = data['CurrentVehicleDataResponse'] 4 | self.request_id = data['requestId'] 5 | self.vin = data['vin'] 6 | -------------------------------------------------------------------------------- /audiapi/model/HonkFlash.py: -------------------------------------------------------------------------------- 1 | class HonkFlashAction: 2 | def __init__(self, data): 3 | request = data.get('honkAndFlashRequest') 4 | self.id = request.get('id') 5 | self.last_updated = request.get('lastUpdated') 6 | self.duration = request.get('serviceDuration') 7 | self.operation = request.get('serviceOperationCode') 8 | self.user_pos = request.get('userPosition') 9 | 10 | 11 | class RemoteHonkFlashActionStatus: 12 | def __init__(self, data): 13 | request = data.get('status') 14 | self.status = request.get('statusCode') 15 | self.reason = request.get('statusReason') 16 | -------------------------------------------------------------------------------- /audiapi/model/OperationsID.py: -------------------------------------------------------------------------------- 1 | class OperationsID: 2 | CAR_FINDER_FIND_CAR = 'FIND_CAR' 3 | GEOFENCING_DELETE_VIOLATION = 'D_ALERT' 4 | GEOFENCING_GET_ALERTS = 'G_ALERTS' 5 | GEOFENCING_GET_DEFINITION_LIST = 'G_DLIST' 6 | GEOFENCING_GET_DEFINITION_LIST_STATUS = 'G_DLSTATUS' 7 | GEOFENCING_SAVE_DEFINITION_LIST = 'P_DLIST' 8 | MOBILE_KEY_ACCEPT_PERMISSION = 'P_PERMISSION_ACCEPT' 9 | MOBILE_KEY_CONFIRM_MOBILE_KEY_CREATION = 'PU_MKCONFIRM' 10 | MOBILE_KEY_CREATE_MOBILE_KEY = 'P_MKCREATE' 11 | MOBILE_KEY_GRANT_PERMISSION_CONFIRMATION_BY_VTAN = 'P_VTAN' 12 | MOBILE_KEY_GRANT_PERMISSION_START_PERMISSION = 'P_PERMISSION' 13 | MOBILE_KEY_READ_KEY_SYSTEM_USER_VIEW = 'G_KEYSYSTEMUSERVIEW' 14 | MOBILE_KEY_READ_MOBILE_KEYS = 'G_MOBILEKEYS' 15 | MOBILE_KEY_READ_PERMISSIONS = 'G_PERMISSIONS' 16 | MOBILE_KEY_READ_SERVICE_STATUS_VIEW = 'G_SERVICESTATUSVIEW' 17 | MOBILE_KEY_RETURN_MOBILE_KEY = 'D_MOBILEKEY' 18 | MOBILE_KEY_RETURN_PERMISSION = 'D_PERMISSION_NUTZER' 19 | MOBILE_KEY_REVOKE_PERMISSION = 'D_PERMISSION' 20 | MOBILE_KEY_UPDATE_PERMISSION_FRONTEND_ALIAS_PERMISSIONS = 'PU_PERMISSION' 21 | REMOTE_BATTERY_CHARGE_GET_STATUS = 'G_STATUS' 22 | REMOTE_BATTERY_CHARGE_START_CHARGING = 'P_START' 23 | REMOTE_BATTERY_CHARGE_START_CHARGING_NOW = 'START_CHARGING_NOW' 24 | REMOTE_BATTERY_CHARGE_START_CHARGING_NO_SET = 'P_START_NOSET' 25 | REMOTE_BATTERY_CHARGE_START_CHARGING_TIMER_BASED = 'START_CHARGING_TIMER_BASED' 26 | REMOTE_DEPARTURE_TIMER_GET_STATUS = 'G_STATUS' 27 | f363x73823fb6 = 'P_SETTINGS_AU' 28 | f364x1ca850bf = 'P_SETTINGS_EL' 29 | REMOTE_HEATING_GET_REQUEST_STATUS = 'G_RQSTAT' 30 | REMOTE_HEATING_GET_STATUS = 'G_STAT' 31 | REMOTE_HEATING_QUICK_START = 'P_QSACT' 32 | REMOTE_HEATING_QUICK_STOP = 'P_QSTOPACT' 33 | REMOTE_HEATING_SET_DEPARTURE_TIMERS = 'P_DTACT' 34 | REMOTE_HONK_AND_FLASH_PERFORM_REQUEST = 'P_VREQ' 35 | REMOTE_HONK_AND_FLASH_REQUEST_HISTORY = 'G_RHIST' 36 | REMOTE_HONK_AND_FLASH_REQUEST_STATUS = 'G_REQSTATUS' 37 | REMOTE_LOCK_UNLOCK_GET_LAST_ACTIONS = 'G_LACT' 38 | REMOTE_LOCK_UNLOCK_GET_REQUEST_STATUS = 'G_RQSTAT' 39 | REMOTE_LOCK_UNLOCK_LOCK = 'LOCK' 40 | REMOTE_LOCK_UNLOCK_UNLOCK = 'UNLOCK' 41 | REMOTE_PRETRIP_CLIMATISATION_GET_STATUS = 'G_STATUS' 42 | REMOTE_PRETRIP_CLIMATISATION_START_AUX_OR_AUTO = 'P_START_CLIMA_AU' 43 | REMOTE_PRETRIP_CLIMATISATION_START_ELECTRIC = 'P_START_CLIMA_EL' 44 | REMOTE_TRIP_STATISTICS_DELETE_STATISTICS = 'D_TRIPDATA' 45 | REMOTE_TRIP_STATISTICS_GET_STATISTICS = 'G_TRIPDATA' 46 | SPEED_ALERT_DELETE_VIOLATION = 'D_ALERT' 47 | SPEED_ALERT_GET_ALERTS = 'G_ALERTS' 48 | SPEED_ALERT_GET_DEFINITION_LIST = 'G_DLIST' 49 | SPEED_ALERT_GET_DEFINITION_LIST_STATUS = 'G_DLSTATUS' 50 | SPEED_ALERT_SAVE_DEFINITION_LIST = 'P_DLIST' 51 | THEFT_ALARM_DELETE_WARNING = 'D_NHIST' 52 | THEFT_ALARM_GET_WARNINGS = 'G_NHIST' 53 | VALET_ALERT_DELETE_DEFINITION = 'D_DEF' 54 | VALET_ALERT_DELETE_VIOLATION = 'D_ALERT' 55 | VALET_ALERT_GET_ALERTS = 'G_ALERTS' 56 | VALET_ALERT_GET_DEFINITION = 'G_DEF' 57 | VALET_ALERT_GET_DEFINITION_STATUS = 'G_DSTATUS' 58 | VALET_ALERT_SAVE_DEFINITION = 'P_DEF' 59 | VEHICLE_STATUS_REPORT_GET_CURRENT_VEHICLE_DATA = 'G_CVDATA' 60 | VEHICLE_STATUS_REPORT_GET_CURRENT_VEHICLE_DATA_BY_ID = 'G_CVDATAID' 61 | VEHICLE_STATUS_REPORT_GET_REQUEST_STATUS = 'G_RQSTAT' 62 | VEHICLE_STATUS_REPORT_GET_STORED_VEHICLE_DATA = 'G_SVDATA' 63 | -------------------------------------------------------------------------------- /audiapi/model/PinAuth.py: -------------------------------------------------------------------------------- 1 | class PinAuthInfoResponse: 2 | def __init__(self): 3 | self.security_token = '' 4 | self.pin_transmission = SecurityPinTransmission() 5 | 6 | 7 | class SecurityPinTransmission: 8 | def __init__(self): 9 | self.challenge = '' 10 | self.hash_procedure_version = 0 11 | self.user_challenge = '' 12 | -------------------------------------------------------------------------------- /audiapi/model/RequestStatus.py: -------------------------------------------------------------------------------- 1 | class RequestStatus: 2 | IN_PROGRESS = 'request_in_progress' 3 | SUCCESS = 'request_successful' 4 | FAIL = 'request_fail' 5 | 6 | def __init__(self, data): 7 | data = data['requestStatusResponse'] 8 | self.status = data['status'] 9 | self.vin = data['vin'] 10 | -------------------------------------------------------------------------------- /audiapi/model/ServiceID.py: -------------------------------------------------------------------------------- 1 | class ServiceID: 2 | APP_MEDIA = 'appmedia_v1' 3 | CALENDAR = 'app_calendar_v1' 4 | CAR_FINDER = 'carfinder_v1' 5 | GEOFENCING = 'geofence_v1' 6 | MOBILE_KEY = 'mobilekey_v1' 7 | MY_AUDI_DESTINATIONS = 'zieleinspeisung_v1' 8 | PICTURE_NAV1 = 'picturenav_v1' 9 | PICTURE_NAV3 = 'picturenav_v3' 10 | REMOTE_BATTERY_CHARGE = 'rbatterycharge_v1' 11 | REMOTE_DEPARTURE_TIMER = 'timerprogramming_v1' 12 | REMOTE_HEATING = 'rheating_v1' 13 | REMOTE_HONK_AND_FLASH = 'rhonk_v1' 14 | REMOTE_LOCK_UNLOCK = 'rlu_v1' 15 | REMOTE_PRETRIP_CLIMATISATION = 'rclima_v1' 16 | REMOTE_TRIP_STATISTICS = 'trip_statistic_v1' 17 | SPEED_ALERT = 'speedalert_v1' 18 | THEFT_ALARM = 'dwap' 19 | TRAVELGUIDE = 'travelguide_v1' 20 | VALET_ALERT = 'valetalert_v1' 21 | VEHICLE_STATUS_REPORT = 'statusreport_v1' 22 | -------------------------------------------------------------------------------- /audiapi/model/Vehicle.py: -------------------------------------------------------------------------------- 1 | class Vehicle: 2 | """ 3 | Represents a single vehicle 4 | """ 5 | 6 | def __init__(self): 7 | self.vin = '' 8 | self.csid = '' 9 | self.registered = '' 10 | 11 | def parse(self, data): 12 | self.vin = data.get('VIN') 13 | self.csid = data.get('CSID') 14 | self.registered = data.get('registered') 15 | 16 | def __str__(self): 17 | return str(self.__dict__) 18 | 19 | 20 | class VehiclesResponse: 21 | def __init__(self): 22 | self.vehicles = [] 23 | """ 24 | List of vehicles 25 | 26 | :type vehicles: List[Vehicle] 27 | """ 28 | self.blacklisted_vins = 0 29 | 30 | def parse(self, data): 31 | response = data.get('getUserVINsResponse') 32 | self.blacklisted_vins = response.get('vinsOnBlacklist') 33 | for item in response.get('CSIDVins'): 34 | vehicle = Vehicle() 35 | vehicle.parse(item) 36 | self.vehicles.append(vehicle) 37 | -------------------------------------------------------------------------------- /audiapi/model/VehicleDataResponse.py: -------------------------------------------------------------------------------- 1 | class VehicleDataResponse: 2 | """ 3 | Response from vehicle detail requests 4 | """ 5 | 6 | def __init__(self, data): 7 | self.data_fields = [] 8 | response = data.get('StoredVehicleDataResponse') 9 | if response is None: 10 | response = data.get('CurrentVehicleDataByRequestResponse') 11 | 12 | vehicle_data = response.get('vehicleData') 13 | if vehicle_data is None: 14 | return 15 | 16 | vehicle_data = vehicle_data.get('data') 17 | for raw_data in vehicle_data: 18 | raw_fields = raw_data.get('field') 19 | if raw_fields is None: 20 | continue 21 | for raw_field in raw_fields: 22 | self.data_fields.append(Field(raw_field)) 23 | 24 | 25 | class Field: 26 | IDS = {'0x0': 'UNKNOWN', 27 | '0x0101010002': 'UTC_TIME_AND_KILOMETER_STATUS', 28 | '0x0203010001': 'MAINTENANCE_INTERVAL_DISTANCE_TO_OIL_CHANGE', 29 | '0x0203010002': 'MAINTENANCE_INTERVAL_TIME_TO_OIL_CHANGE', 30 | '0x0203010003': 'MAINTENANCE_INTERVAL_DISTANCE_TO_INSPECTION', 31 | '0x0203010004': 'MAINTENANCE_INTERVAL_TIME_TO_INSPECTION', 32 | '0x0203010006': 'MAINTENANCE_INTERVAL_ALARM_INSPECTION', 33 | '0x0203010007': 'MAINTENANCE_INTERVAL_MONTHLY_MILEAGE', 34 | '0x0203010005': 'WARNING_OIL_CHANGE', 35 | '0x0204040001': 'OIL_LEVEL_AMOUNT_IN_LITERS', 36 | '0x0204040002': 'OIL_LEVEL_MINIMUM_WARNING', 37 | '0x0204040003': 'OIL_LEVEL_DIPSTICKS_PERCENTAGE', 38 | '0x02040C0001': 'ADBLUE_RANGE', 39 | '0x0301010001': 'LIGHT_STATUS', 40 | '0x0301030001': 'BRAKING_STATUS', 41 | '0x0301030005': 'TOTAL_RANGE', 42 | '0x030103000A': 'TANK_LEVEL_IN_PERCENTAGE', 43 | '0x0301040001': 'LOCK_STATE_LEFT_FRONT_DOOR', 44 | '0x0301040002': 'OPEN_STATE_LEFT_FRONT_DOOR', 45 | '0x0301040003': 'SAFETY_STATE_LEFT_FRONT_DOOR', 46 | '0x0301040004': 'LOCK_STATE_LEFT_REAR_DOOR', 47 | '0x0301040005': 'OPEN_STATE_LEFT_REAR_DOOR', 48 | '0x0301040006': 'SAFETY_STATE_LEFT_REAR_DOOR', 49 | '0x0301040007': 'LOCK_STATE_RIGHT_FRONT_DOOR', 50 | '0x0301040008': 'OPEN_STATE_RIGHT_FRONT_DOOR', 51 | '0x0301040009': 'SAFETY_STATE_RIGHT_FRONT_DOOR', 52 | '0x030104000A': 'LOCK_STATE_RIGHT_REAR_DOOR', 53 | '0x030104000B': 'OPEN_STATE_RIGHT_REAR_DOOR', 54 | '0x030104000C': 'SAFETY_STATE_RIGHT_REAR_DOOR', 55 | '0x030104000D': 'LOCK_STATE_TRUNK_LID', 56 | '0x030104000E': 'OPEN_STATE_TRUNK_LID', 57 | '0x030104000F': 'SAFETY_STATE_TRUNK_LID', 58 | '0x0301040010': 'LOCK_STATE_HOOD', 59 | '0x0301040011': 'OPEN_STATE_HOOD', 60 | '0x0301040012': 'SAFETY_STATE_HOOD', 61 | '0x0301050001': 'STATE_LEFT_FRONT_WINDOW', 62 | '0x0301050003': 'STATE_LEFT_REAR_WINDOW', 63 | '0x0301050005': 'STATE_RIGHT_FRONT_WINDOW', 64 | '0x0301050007': 'STATE_RIGHT_REAR_WINDOW', 65 | '0x0301050009': 'STATE_DECK', 66 | '0x030105000B': 'STATE_SUN_ROOF_MOTOR_COVER', 67 | '0x0301030006': 'PRIMARY_RANGE', 68 | '0x0301030007': 'PRIMARY_DRIVE', 69 | '0x0301030008': 'SECONDARY_RANGE', 70 | '0x0301030009': 'SECONDARY_DRIVE', 71 | '0x0301030002': 'STATE_OF_CHARGE', 72 | '0x0301020001': 'TEMPERATURE_OUTSIDE', 73 | '0x0202': 'ACTIVE_INSTRUMENT_CLUSTER_WARNING'} 74 | 75 | def __init__(self, data): 76 | self.name = None 77 | self.id = data.get('id') 78 | self.unit = data.get('unit') 79 | self.value = data.get('value') 80 | self.measure_time = data.get('tsCarCaptured') 81 | self.send_time = data.get('tsCarSent') 82 | self.measure_mileage = data.get('milCarCaptured') 83 | self.send_mileage = data.get('milCarSent') 84 | 85 | for field_id, name in self.IDS.items(): 86 | if field_id == self.id: 87 | self.name = name 88 | break 89 | if self.name is None: 90 | # No direct mapping found - maybe we've at least got a text id 91 | self.name = data.get('textId') 92 | 93 | def __str__(self): 94 | str_rep = str(self.name) + ' ' + str(self.value) 95 | if self.unit is not None: 96 | str_rep += self.unit 97 | return str_rep 98 | -------------------------------------------------------------------------------- /audiapi/model/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/davidgiga1993/AudiAPI/f3f629c45c484c25eb75ffebca03b743e09a1cdc/audiapi/model/__init__.py -------------------------------------------------------------------------------- /credentials.template.json: -------------------------------------------------------------------------------- 1 | { 2 | "user": "yourUser@mail.com", 3 | "pass": "yourPassword" 4 | } -------------------------------------------------------------------------------- /publish.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | rm dist/* 4 | python setup.py sdist bdist_wheel 5 | # twine upload --repository-url https://test.pypi.org/legacy/ dist/* 6 | twine upload dist/* -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | import setuptools 2 | 3 | with open('README.md', 'r') as fh: 4 | long_description = fh.read() 5 | 6 | setuptools.setup( 7 | name='audiapi', 8 | version='1.0.1', 9 | author='davidgiga1993', 10 | author_email='david@dev-core.org', 11 | description='Provides access to the Audi Connect API', 12 | long_description=long_description, 13 | long_description_content_type='text/markdown', 14 | url='https://github.com/davidgiga1993/AudiAPI', 15 | packages=setuptools.find_packages(), 16 | classifiers=[ 17 | 'Development Status :: 4 - Beta', 18 | 'Programming Language :: Python :: 3', 19 | 'License :: OSI Approved :: MIT License', 20 | 'Operating System :: OS Independent', 21 | 'Topic :: Software Development :: Libraries' 22 | ], 23 | ) 24 | --------------------------------------------------------------------------------