├── .bumpversion.cfg ├── .travis.yml ├── LICENSE ├── README.md ├── leafpy ├── __init__.py ├── auth.py └── leaf.py ├── requirements.txt ├── setup.py ├── tests ├── __init__.py └── unit │ ├── __pycache__ │ ├── test_api_calls.cpython-27-PYTEST.pyc │ └── test_login.cpython-27-PYTEST.pyc │ ├── cassettes │ ├── test_call_to_nonexistent_endpoint.yaml │ ├── test_call_with_no_params.yaml │ ├── test_call_with_params.yaml │ ├── test_exception_raised_when_bad_vin_and_customsessionid_used.yaml │ ├── test_exeption_raised_when_bad_credentials_passed.yaml │ ├── test_login.yaml │ └── test_login_standalone.yaml │ ├── test_api_calls.py │ └── test_login.py └── unit_tests.py /.bumpversion.cfg: -------------------------------------------------------------------------------- 1 | [bumpversion] 2 | current_version = 0.2.6 3 | commit = True 4 | tag = True 5 | tag_name = {new_version} 6 | 7 | [bumpversion:file:setup.py] 8 | 9 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | python: 3 | - "2.7" 4 | - "3.4" 5 | - "3.5" 6 | - "3.6" 7 | # command to install dependencies 8 | install: 9 | - pip install -r requirements.txt codecov 10 | # command to run tests 11 | script: coverage run unit_tests.py 12 | after_success: 13 | - bash <(curl -s https://codecov.io/bash) 14 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Nate Ricklin 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # leafpy 2 | Lightweight python interface to the nissan leaf. Check battery status, turn on the AC, start charging, etc. 3 | 4 | [![PyPI version](https://badge.fury.io/py/leafpy.svg)](https://badge.fury.io/py/leafpy) 5 | [![Build Status](https://travis-ci.org/nricklin/leafpy.svg?branch=master)](https://travis-ci.org/nricklin/leafpy) 6 | [![codecov](https://codecov.io/gh/nricklin/leafpy/branch/master/graph/badge.svg)](https://codecov.io/gh/nricklin/leafpy) 7 | 8 | 9 | # Installation 10 | ``` 11 | pip install leafpy 12 | ``` 13 | 14 | # Examples: 15 | 16 | Login: 17 | ---- 18 | You need to add the region_code if you're not from US. 19 | * COUNTRY_CANADA = 'NCI' 20 | * COUNTRY_US = 'NNA' 21 | * COUNTRY_EU = 'NE' 22 | * COUNTRY_AUSTRALIA = 'NMA' 23 | * COUNTRY_JAPAN = 'NML' 24 | 25 | ```python 26 | from leafpy import Leaf 27 | leaf = Leaf('', '', region_code = 'NE') 28 | ``` 29 | 30 | Or with custom_sessionid & VIN: 31 | 32 | ```python 33 | from leafpy import Leaf 34 | leaf = Leaf(custom_sesionid='', VIN='') 35 | # you can get these values from a previous login: 36 | # leaf.VIN, leaf.custom_sessionid 37 | ``` 38 | 39 | *Check Battery Status:* 40 | ----- 41 | ```python 42 | leaf.BatteryStatusRecordsRequest() 43 | ``` 44 | results in: 45 | ```json 46 | { 47 | "status": 200, 48 | "BatteryStatusRecords": { 49 | "BatteryStatus": { 50 | "BatteryRemainingAmountkWH": "", 51 | "SOC": { 52 | "Value": "100" 53 | }, 54 | "BatteryChargingStatus": "NOT_CHARGING", 55 | "BatteryRemainingAmount": "240", 56 | "BatteryCapacity": "240", 57 | "BatteryRemainingAmountWH": "28880" 58 | }, 59 | "OperationResult": "START", 60 | "NotificationDateAndTime": "2017/04/24 23:43", 61 | "CruisingRangeAcOff": "198000", 62 | "OperationDateAndTime": "2017/04/24 23:43", 63 | "CruisingRangeAcOn": "192000", 64 | "PluginState": "CONNECTED", 65 | "TargetDate": "2017/04/24 23:43" 66 | }, 67 | "VoltLabel": { 68 | "HighVolt": "240", 69 | "LowVolt": "120" 70 | } 71 | } 72 | ``` 73 | *Query for Battery Status:* 74 | ----- 75 | ```python 76 | response = leaf.BatteryStatusCheckRequest() 77 | # Wait a few seconds for the request to be made to the car 78 | leaf.BatteryStatusCheckResultRequest(resultKey=response['resultKey']) 79 | ``` 80 | results in: 81 | ```json 82 | { 83 | "status": 200, 84 | "currentChargeLevel": "0", 85 | "timeRequiredToFull": { 86 | "hours": "", 87 | "minutes": "" 88 | }, 89 | "timeRequiredToFull200_6kW": { 90 | "hours": "", 91 | "minutes": "" 92 | }, 93 | "operationResult": "START", 94 | "timeStamp": "2017-04-25 05:23:48", 95 | "pluginState": "CONNECTED", 96 | "cruisingRangeAcOff": "198000.0", 97 | "timeRequiredToFull200": { 98 | "hours": "", 99 | "minutes": "" 100 | }, 101 | "batteryCapacity": "240", 102 | "cruisingRangeAcOn": "192000.0", 103 | "responseFlag": "1", 104 | "batteryDegradation": "240", 105 | "charging": "NO", 106 | "chargeStatus": "CT", 107 | "chargeMode": "NOT_CHARGING" 108 | } 109 | ``` 110 | 111 | *Turn on Climate Control:* 112 | ----- 113 | ```python 114 | response = leaf.ACRemoteRequest() 115 | # Wait a few seconds for the request to be made to the car 116 | # Check status if you don't want to assume it worked: 117 | leaf.ACRemoteResult(resultKey=response['resultKey']) 118 | ``` 119 | results in: 120 | ```json 121 | { 122 | "status": 200, 123 | "hvacStatus": "ON", 124 | "operationResult": "START", 125 | "timeStamp": "2017-04-25 05:38:09", 126 | "acContinueTime": "7200", 127 | "responseFlag": "1" 128 | } 129 | ``` 130 | 131 | *Turn off Climate Control:* 132 | ----- 133 | ```python 134 | response = leaf.ACRemoteOffRequest() 135 | # Wait a few seconds for the request to be made to the car 136 | # Check status if you don't want to assume it worked: 137 | leaf.ACRemoteOffResult(resultKey=response['resultKey']) 138 | ``` 139 | results in: 140 | ```json 141 | { 142 | "status": 200, 143 | "timeStamp": "2017-04-25 05:40:27", 144 | "hvacStatus": "OFF", 145 | "operationResult": "START", 146 | "responseFlag": "1" 147 | } 148 | ``` 149 | 150 | *Get latest climate control status:* 151 | ----- 152 | ```python 153 | leaf.RemoteACRecordsRequest() 154 | ``` 155 | results in: 156 | ```json 157 | { 158 | "status": 200, 159 | "RemoteACRecords": { 160 | "ACDurationPluggedSec": "7200", 161 | "ACStartStopDateAndTime": "2017/04/25 05:40", 162 | "ACStartStopURL": "", 163 | "PreAC_temp": "75", 164 | "OperationResult": "START", 165 | "PreAC_unit": "F", 166 | "OperationDateAndTime": "2017/04/25 05:40", 167 | "PluginState": "CONNECTED", 168 | "ACDurationBatterySec": "900", 169 | "RemoteACOperation": "STOP" 170 | } 171 | } 172 | ``` 173 | 174 | *Get Car Location:* 175 | ----- 176 | Apparently Deprecated 177 | 178 | 179 | *Start Charging:* 180 | ----- 181 | ```python 182 | leaf.BatteryRemoteChargingRequest() 183 | ``` 184 | 185 | *Schedule Climate Control:* 186 | ----- 187 | ```python 188 | leaf.ACRemoteNewRequest(ExecuteTime='2016-02-09 17:24') 189 | ``` 190 | 191 | *Update Scheduled Climate Control:* 192 | ----- 193 | ```python 194 | leaf.ACRemoteUpdateRequest(ExecuteTime='2016-02-09 17:24') 195 | ``` 196 | 197 | *Cancel Scheduled Climate Control:* 198 | ----- 199 | ```python 200 | leaf.ACRemoteCancelRequest() 201 | ``` 202 | 203 | *Get Climate Control Schedule:* 204 | ----- 205 | ```python 206 | leaf.GetScheduledACRemoteRequest() 207 | ``` 208 | 209 | *Get Driving Analysis:* 210 | ----- 211 | ```python 212 | leaf.DriveAnalysisBasicScreenRequestEx() 213 | ``` 214 | 215 | *Get Price Simulation:* 216 | ----- 217 | ```python 218 | leaf.PriceSimulatorDetailInfoRequest() 219 | ``` 220 | -------------------------------------------------------------------------------- /leafpy/__init__.py: -------------------------------------------------------------------------------- 1 | from .leaf import Leaf -------------------------------------------------------------------------------- /leafpy/auth.py: -------------------------------------------------------------------------------- 1 | """ 2 | Logging in basically means getting the custom_sessionid and VIN, which are used to make 3 | every subsequent request. 4 | """ 5 | 6 | from Crypto.Cipher import Blowfish 7 | import requests 8 | import base64 9 | 10 | def login(username, password, region_code='NNA', initial_app_strings='9s5rfKVuMrT03RtzajWNcA'): 11 | baseprm = b'88dSp7wWnV3bvv9Z88zEwg' 12 | c1 = Blowfish.new(baseprm, Blowfish.MODE_ECB) 13 | packingLength = 8 - len(password) % 8 14 | packedPassword = password + chr(packingLength) * packingLength 15 | encryptedPassword = c1.encrypt(packedPassword.encode('latin-1')) 16 | encodedPassword = base64.standard_b64encode(encryptedPassword) 17 | 18 | url = "https://gdcportalgw.its-mo.com/api_v200413_NE/gdc/UserLoginRequest.php" 19 | data = { 20 | "RegionCode": region_code, 21 | "UserId": username, 22 | "initial_app_str": initial_app_strings, 23 | "Password": encodedPassword, 24 | } 25 | headers = {'User-Agent': 'Mozilla/5.0'} 26 | 27 | r = requests.post(url,data=data, headers=headers) 28 | r.raise_for_status() 29 | if not r.json()['status'] == 200: 30 | raise Exception('Cannot login. Probably username & password are wrong. ' + r.text) 31 | 32 | custom_sessionid = r.json()['VehicleInfoList']['vehicleInfo'][0]['custom_sessionid'] 33 | VIN = r.json()['CustomerInfo']['VehicleInfo']['VIN'] 34 | 35 | return custom_sessionid, VIN 36 | -------------------------------------------------------------------------------- /leafpy/leaf.py: -------------------------------------------------------------------------------- 1 | from .auth import login 2 | import requests 3 | 4 | BASE_URL = 'https://gdcportalgw.its-mo.com/api_v200413_NE/gdc/' 5 | 6 | class Leaf(object): 7 | """Make requests to the Nissan Connect API to get Leaf Info""" 8 | custom_sessionid = None 9 | VIN = None 10 | region_code = None 11 | 12 | def __init__(self, username=None, password=None, custom_sessionid=None, VIN=None, region_code='NNA'): 13 | 14 | self.region_code = region_code 15 | 16 | if username and password: 17 | self.custom_sessionid, self.VIN = login(username, password, self.region_code) 18 | elif custom_sessionid and VIN: 19 | self.custom_sessionid = custom_sessionid 20 | self.VIN = VIN 21 | else: 22 | raise Exception('Need either username & password or custom_sessionid & VIN.') 23 | 24 | def __getattr__(self, name): 25 | """ 26 | Top secret magic. Calling Leaf.() hits .php 27 | """ 28 | 29 | if name.startswith('__'): 30 | raise AttributeError(name) 31 | 32 | def call(**kwargs): 33 | url = BASE_URL + name + '.php' 34 | data = { 35 | "RegionCode": self.region_code, 36 | "custom_sessionid": self.custom_sessionid, 37 | "VIN": self.VIN 38 | } 39 | for k in kwargs: 40 | data[k] = kwargs[k] 41 | r = requests.post(url, data=data) 42 | r.raise_for_status() 43 | if not r.json()['status'] == 200: 44 | raise Exception('Error making request. Perhaps the session has expired.') 45 | return r.json() 46 | 47 | return call 48 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | requests 2 | pycryptodome 3 | vcrpy 4 | pytest 5 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | 2 | import sys 3 | from setuptools import setup, find_packages 4 | 5 | open_kwds = {} 6 | if sys.version_info > (3,): 7 | open_kwds['encoding'] = 'utf-8' 8 | 9 | setup(name='leafpy', 10 | version='0.2.6', 11 | description='Lightweight python interface to the nissan leaf.', 12 | classifiers=[], 13 | keywords='', 14 | author='Nate Ricklin', 15 | author_email='nate.ricklin@gmail.com', 16 | url='https://github.com/nricklin/leafpy', 17 | license='MIT', 18 | packages=find_packages(exclude=['docs','tests','examples']), 19 | include_package_data=True, 20 | zip_safe=False, 21 | install_requires=['requests','pycryptodome'], 22 | setup_requires=['pytest-runner'], 23 | tests_require=['pytest','vcrpy'] 24 | ) 25 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nricklin/leafpy/1a8b72dece87dbee3ab11336053d0d275fa539bd/tests/__init__.py -------------------------------------------------------------------------------- /tests/unit/__pycache__/test_api_calls.cpython-27-PYTEST.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nricklin/leafpy/1a8b72dece87dbee3ab11336053d0d275fa539bd/tests/unit/__pycache__/test_api_calls.cpython-27-PYTEST.pyc -------------------------------------------------------------------------------- /tests/unit/__pycache__/test_login.cpython-27-PYTEST.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nricklin/leafpy/1a8b72dece87dbee3ab11336053d0d275fa539bd/tests/unit/__pycache__/test_login.cpython-27-PYTEST.pyc -------------------------------------------------------------------------------- /tests/unit/cassettes/test_call_to_nonexistent_endpoint.yaml: -------------------------------------------------------------------------------- 1 | interactions: 2 | - request: 3 | body: !!python/unicode 'RegionCode=NNA' 4 | headers: 5 | Accept: ['*/*'] 6 | Accept-Encoding: ['gzip, deflate'] 7 | Connection: [keep-alive] 8 | Content-Length: ['170'] 9 | Content-Type: [application/x-www-form-urlencoded] 10 | User-Agent: [python-requests/2.13.0] 11 | method: POST 12 | uri: https://gdcportalgw.its-mo.com/api_v190426_NE/gdc/this_doesnt_exist.php 13 | response: 14 | body: {string: !!python/unicode ' 15 | 16 | 17 | 18 | 404 Not Found 19 | 20 | 21 | 22 |

Not Found

23 | 24 |

The requested URL /api_v190426_NE/gdc/this_doesnt_exist.php was not found 25 | on this server.

26 | 27 | 28 | 29 | '} 30 | headers: 31 | connection: [close] 32 | content-length: ['238'] 33 | content-type: [text/html; charset=iso-8859-1] 34 | date: ['Thu, 05 Sep 2019 22:45:26 GMT'] 35 | server: [Apache] 36 | status: {code: 404, message: Not Found} 37 | version: 1 38 | -------------------------------------------------------------------------------- /tests/unit/cassettes/test_call_with_no_params.yaml: -------------------------------------------------------------------------------- 1 | interactions: 2 | - request: 3 | body: !!python/unicode 'RegionCode=NNA' 4 | headers: 5 | Accept: ['*/*'] 6 | Accept-Encoding: ['gzip, deflate'] 7 | Connection: [keep-alive] 8 | Content-Length: ['170'] 9 | Content-Type: [application/x-www-form-urlencoded] 10 | User-Agent: [python-requests/2.13.0] 11 | method: POST 12 | uri: https://gdcportalgw.its-mo.com/api_v190426_NE/gdc/BatteryStatusRecordsRequest.php 13 | response: 14 | body: {string: !!python/unicode '{"status":200,"VoltLabel":{"HighVolt":"240","LowVolt":"120"},"BatteryStatusRecords":{"OperationResult":"START","OperationDateAndTime":"2019\/09\/05 15 | 18:37","BatteryStatus":{"BatteryChargingStatus":"NOT_CHARGING","BatteryCapacity":"240","BatteryRemainingAmount":"240","BatteryRemainingAmountWH":"22880","BatteryRemainingAmountkWH":"","SOC":{"Value":"100"}},"PluginState":"CONNECTED","CruisingRangeAcOn":"152000","CruisingRangeAcOff":"158000","NotificationDateAndTime":"2019\/09\/05 16 | 18:37","TargetDate":"2019\/09\/05 18:37"}}'} 17 | headers: 18 | connection: [close] 19 | content-type: [application/json; charset=utf-8] 20 | date: ['Thu, 05 Sep 2019 22:45:27 GMT'] 21 | server: [Apache] 22 | strict-transport-security: [max-age=15768000] 23 | status: {code: 200, message: OK} 24 | version: 1 25 | -------------------------------------------------------------------------------- /tests/unit/cassettes/test_call_with_params.yaml: -------------------------------------------------------------------------------- 1 | interactions: 2 | - request: 3 | body: !!python/unicode 'RegionCode=NNA' 4 | headers: 5 | Accept: ['*/*'] 6 | Accept-Encoding: ['gzip, deflate'] 7 | Connection: [keep-alive] 8 | Content-Length: ['170'] 9 | Content-Type: [application/x-www-form-urlencoded] 10 | User-Agent: [python-requests/2.13.0] 11 | method: POST 12 | uri: https://gdcportalgw.its-mo.com/api_v190426_NE/gdc/BatteryStatusCheckRequest.php 13 | response: 14 | body: {string: !!python/unicode '{"status":200,"userId":"testuser@gmail.com","vin":"vin123","resultKey":"asdf"}'} 15 | headers: 16 | connection: [close] 17 | content-type: [application/json; charset=utf-8] 18 | date: ['Thu, 05 Sep 2019 22:45:28 GMT'] 19 | server: [Apache] 20 | strict-transport-security: [max-age=15768000] 21 | status: {code: 200, message: OK} 22 | - request: 23 | body: !!python/unicode 'RegionCode=NNA&resultKey=asdf' 24 | headers: 25 | Accept: ['*/*'] 26 | Accept-Encoding: ['gzip, deflate'] 27 | Connection: [keep-alive] 28 | Content-Length: ['231'] 29 | Content-Type: [application/x-www-form-urlencoded] 30 | User-Agent: [python-requests/2.13.0] 31 | method: POST 32 | uri: https://gdcportalgw.its-mo.com/api_v190426_NE/gdc/BatteryStatusCheckResultRequest.php 33 | response: 34 | body: {string: !!python/unicode '{"status":200,"responseFlag":"0"}'} 35 | headers: 36 | connection: [close] 37 | content-type: [application/json; charset=utf-8] 38 | date: ['Thu, 05 Sep 2019 22:47:49 GMT'] 39 | server: [Apache] 40 | strict-transport-security: [max-age=15768000] 41 | status: {code: 200, message: OK} 42 | version: 1 43 | -------------------------------------------------------------------------------- /tests/unit/cassettes/test_exception_raised_when_bad_vin_and_customsessionid_used.yaml: -------------------------------------------------------------------------------- 1 | interactions: 2 | - request: 3 | body: !!python/unicode 'RegionCode=NNA&VIN=vin345&custom_sessionid=csid123' 4 | headers: 5 | Accept: ['*/*'] 6 | Accept-Encoding: ['gzip, deflate'] 7 | Connection: [keep-alive] 8 | Content-Length: ['50'] 9 | Content-Type: [application/x-www-form-urlencoded] 10 | User-Agent: [python-requests/2.13.0] 11 | method: POST 12 | uri: https://gdcportalgw.its-mo.com/api_v190426_NE/gdc/BatteryStatusRecordsRequest.php 13 | response: 14 | body: {string: !!python/unicode '{"status":404}'} 15 | headers: 16 | connection: [close] 17 | content-type: [text/html; charset=UTF-8] 18 | date: ['Thu, 05 Sep 2019 22:34:48 GMT'] 19 | server: [Apache] 20 | strict-transport-security: [max-age=15768000] 21 | status: {code: 200, message: OK} 22 | version: 1 23 | -------------------------------------------------------------------------------- /tests/unit/cassettes/test_exeption_raised_when_bad_credentials_passed.yaml: -------------------------------------------------------------------------------- 1 | interactions: 2 | - request: 3 | body: !!python/unicode 'RegionCode=NNA&initial_app_str=9s5rfKVuMrT03RtzajWNcA' 4 | headers: 5 | Accept: ['*/*'] 6 | Accept-Encoding: ['gzip, deflate'] 7 | Connection: [keep-alive] 8 | Content-Length: ['121'] 9 | Content-Type: [application/x-www-form-urlencoded] 10 | User-Agent: [Mozilla/5.0] 11 | method: POST 12 | uri: https://gdcportalgw.its-mo.com/api_v190426_NE/gdc/UserLoginRequest.php 13 | response: 14 | body: {string: !!python/unicode '{"status":"-2010","message":"INVALID PARAMS","resultKey":""}'} 15 | headers: 16 | connection: [close] 17 | content-type: [application/json; charset=utf-8] 18 | date: ['Thu, 05 Sep 2019 22:34:49 GMT'] 19 | server: [Apache] 20 | strict-transport-security: [max-age=15768000] 21 | status: {code: 200, message: OK} 22 | version: 1 23 | -------------------------------------------------------------------------------- /tests/unit/cassettes/test_login.yaml: -------------------------------------------------------------------------------- 1 | interactions: 2 | - request: 3 | body: !!python/unicode 'RegionCode=NNA&initial_app_str=9s5rfKVuMrT03RtzajWNcA' 4 | headers: 5 | Accept: ['*/*'] 6 | Accept-Encoding: ['gzip, deflate'] 7 | Connection: [keep-alive] 8 | Content-Length: ['123'] 9 | Content-Type: [application/x-www-form-urlencoded] 10 | User-Agent: [Mozilla/5.0] 11 | method: POST 12 | uri: https://gdcportalgw.its-mo.com/api_v190426_NE/gdc/UserLoginRequest.php 13 | response: 14 | body: {string: !!python/unicode '{"status":200,"sessionId":"asdfasdfasdf","VehicleInfoList":{"VehicleInfo":[{"charger20066":"false","nickname":"LEAF2017","telematicsEnabled":"true","vin":"vin123"}],"vehicleInfo":[{"charger20066":"false","nickname":"LEAF2017","telematicsEnabled":"true","vin":"vin123","custom_sessionid":"csessid"}]},"vehicle":{"profile":{"vin":"vin123","gdcUserId":"","gdcPassword":"","encAuthToken":"asdfadsf","dcmId":"fdsa","nickname":"fdsa","status":"ACCEPTED","statusDate":"2017\/04\/15 15 | 00:00"}},"EncAuthToken":"fdsa","CustomerInfo":{"UserId":"sdfd","Language":"en-US","Timezone":"asdf","RegionCode":"NNA","OwnerId":"asdf","EMailAddress":"testuser@gmail.com","Nickname":"aasdf","Country":"US","VehicleImage":"\/content\/language\/default\/images\/img\/ph_car.jpg","UserVehicleBoundDurationSec":"946771200","VehicleInfo":{"VIN":"vin123","DCMID":"fdsa","SIMID":"asdf","NAVIID":"fdsa","EncryptedNAVIID":"asdf","MSN":"asdf","LastVehicleLoginTime":"","UserVehicleBoundTime":"2017-04-15T21:24:23Z","LastDCMUseTime":"","NonaviFlg":"false","CarName":"LEAF","CarImage":"carimg1.png"}},"UserInfoRevisionNo":"1","ngTapUpdatebtn":"300000","timeoutUpdateAnime":"300000","G1Lw":"5","G1Li":"2","G1Lt":"20","G1Uw":"15","G1Ui":"2","G1Ut":"20","G2Lw":"15","G2Li":"2","G2Lt":"20","G2Uw":"15","G2Ui":"2","G2Ut":"20","resultKey":"aaa"}'} 16 | headers: 17 | connection: [close] 18 | content-type: [application/json; charset=utf-8] 19 | date: ['Thu, 05 Sep 2019 22:34:52 GMT'] 20 | server: [Apache] 21 | strict-transport-security: [max-age=15768000] 22 | status: {code: 200, message: OK} 23 | version: 1 24 | -------------------------------------------------------------------------------- /tests/unit/cassettes/test_login_standalone.yaml: -------------------------------------------------------------------------------- 1 | interactions: 2 | - request: 3 | body: !!python/unicode 'RegionCode=NNA&initial_app_str=9s5rfKVuMrT03RtzajWNcA' 4 | headers: 5 | Accept: ['*/*'] 6 | Accept-Encoding: ['gzip, deflate'] 7 | Connection: [keep-alive] 8 | Content-Length: ['123'] 9 | Content-Type: [application/x-www-form-urlencoded] 10 | User-Agent: [Mozilla/5.0] 11 | method: POST 12 | uri: https://gdcportalgw.its-mo.com/api_v190426_NE/gdc/UserLoginRequest.php 13 | response: 14 | body: {string: !!python/unicode '{"status":200,"sessionId":"asdf","VehicleInfoList":{"VehicleInfo":[{"charger20066":"false","nickname":"LEAF2017","telematicsEnabled":"true","vin":"vin123"}],"vehicleInfo":[{"charger20066":"false","nickname":"LEAF2017","telematicsEnabled":"true","vin":"vin123","custom_sessionid":"csessid"}]},"vehicle":{"profile":{"vin":"vin123","gdcUserId":"","gdcPassword":"","encAuthToken":"asdf","dcmId":"a","nickname":"b","status":"ACCEPTED","statusDate":"2017\/04\/15 15 | 00:00"}},"EncAuthToken":"aaa","CustomerInfo":{"UserId":"bbb","Language":"en-US","Timezone":"America\/Denver","RegionCode":"NNA","OwnerId":"ccc","EMailAddress":"testuser@gmail.com","Nickname":"Nathan377","Country":"US","VehicleImage":"\/content\/language\/default\/images\/img\/ph_car.jpg","UserVehicleBoundDurationSec":"946771200","VehicleInfo":{"VIN":"vin123","DCMID":"aaa","SIMID":"bbb","NAVIID":"c","EncryptedNAVIID":"d","MSN":"e","LastVehicleLoginTime":"","UserVehicleBoundTime":"2017-04-15T21:24:23Z","LastDCMUseTime":"","NonaviFlg":"false","CarName":"LEAF","CarImage":"carimg1.png"}},"UserInfoRevisionNo":"1","ngTapUpdatebtn":"300000","timeoutUpdateAnime":"300000","G1Lw":"5","G1Li":"2","G1Lt":"20","G1Uw":"15","G1Ui":"2","G1Ut":"20","G2Lw":"15","G2Li":"2","G2Lt":"20","G2Uw":"15","G2Ui":"2","G2Ut":"20","resultKey":"asdf"}'} 16 | headers: 17 | connection: [close] 18 | content-type: [application/json; charset=utf-8] 19 | date: ['Thu, 05 Sep 2019 22:34:59 GMT'] 20 | server: [Apache] 21 | strict-transport-security: [max-age=15768000] 22 | status: {code: 200, message: OK} 23 | version: 1 24 | -------------------------------------------------------------------------------- /tests/unit/test_api_calls.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | from leafpy import Leaf 3 | import vcr, time 4 | 5 | VIN = 'dummyvin' 6 | custom_sessionid = 'dummy_csid' 7 | 8 | class APICallTests(unittest.TestCase): 9 | 10 | @vcr.use_cassette('tests/unit/cassettes/test_call_with_no_params.yaml', 11 | filter_post_data_parameters=['VIN','custom_sessionid']) 12 | def test_call_with_no_params(self): 13 | leaf = Leaf(VIN=VIN, custom_sessionid=custom_sessionid) 14 | 15 | result = leaf.BatteryStatusRecordsRequest() 16 | 17 | assert result['status'] == 200 18 | 19 | @vcr.use_cassette('tests/unit/cassettes/test_call_with_params.yaml', 20 | filter_post_data_parameters=['VIN','custom_sessionid']) 21 | def test_call_with_params(self): 22 | leaf = Leaf(VIN=VIN, custom_sessionid=custom_sessionid) 23 | 24 | response = leaf.BatteryStatusCheckRequest() 25 | assert response['status'] == 200 26 | #time.sleep(140) # only need to pause here when running against live service 27 | response2 = leaf.BatteryStatusCheckResultRequest(resultKey=response['resultKey']) 28 | assert response2['status'] == 200 29 | 30 | @vcr.use_cassette('tests/unit/cassettes/test_call_to_nonexistent_endpoint.yaml', 31 | filter_post_data_parameters=['VIN','custom_sessionid']) 32 | def test_call_to_nonexistent_endpoint(self): 33 | leaf = Leaf(VIN=VIN, custom_sessionid=custom_sessionid) 34 | 35 | with self.assertRaises(Exception): 36 | response = leaf.this_doesnt_exist() -------------------------------------------------------------------------------- /tests/unit/test_login.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | from leafpy import Leaf 3 | from leafpy.auth import login 4 | import vcr 5 | 6 | USERNAME = 'dummyuser' 7 | PASSWORD = 'dummypass' 8 | 9 | class LoginTests(unittest.TestCase): 10 | 11 | @vcr.use_cassette('tests/unit/cassettes/test_login.yaml', 12 | filter_post_data_parameters=['UserId','Password']) 13 | def test_login(self): 14 | leaf = Leaf(USERNAME, PASSWORD) 15 | 16 | assert leaf.VIN == "vin123" 17 | assert leaf.custom_sessionid == "csessid" 18 | 19 | def test_login_with_custom_sessionid_and_vin(self): 20 | leaf = Leaf(VIN='vin345', custom_sessionid='csid123') 21 | 22 | assert leaf.VIN == 'vin345' 23 | assert leaf.custom_sessionid == 'csid123' 24 | 25 | @vcr.use_cassette('tests/unit/cassettes/test_exeption_raised_when_bad_credentials_passed.yaml', 26 | filter_post_data_parameters=['UserId','Password']) 27 | def test_exeption_raised_when_bad_credentials_passed(self): 28 | with self.assertRaises(Exception) as w: 29 | leaf = Leaf('bad_email@domain.com','invalidpassword') 30 | 31 | @vcr.use_cassette('tests/unit/cassettes/test_exception_raised_when_bad_vin_and_customsessionid_used.yaml', 32 | filter_post_data_parameters=['UserId','Password']) 33 | def test_exception_raised_when_bad_vin_and_customsessionid_used(self): 34 | 35 | leaf = Leaf(VIN='vin345',custom_sessionid='csid123') 36 | 37 | with self.assertRaises(Exception) as w: 38 | leaf.BatteryStatusRecordsRequest() 39 | 40 | def test_login_with_only_username_raises_exception(self): 41 | with self.assertRaises(Exception): 42 | leaf = Leaf('username') 43 | 44 | def test_login_with_only_VIN_raises_exception(self): 45 | with self.assertRaises(Exception): 46 | leaf = Leaf(VIN='vin123') 47 | 48 | def test_login_with_only_custom_sessionid_raises_exception(self): 49 | with self.assertRaises(Exception): 50 | leaf = Leaf(custom_sessionid='vin123') 51 | 52 | def test_login_with_no_args_raises_exception(self): 53 | with self.assertRaises(Exception): 54 | leaf = Leaf() 55 | 56 | @vcr.use_cassette('tests/unit/cassettes/test_login_standalone.yaml', 57 | filter_post_data_parameters=['UserId','Password']) 58 | def test_login_standalone(self): 59 | csid, VIN = login(USERNAME,PASSWORD) 60 | 61 | assert csid == 'csessid' 62 | assert VIN == 'vin123' 63 | 64 | -------------------------------------------------------------------------------- /unit_tests.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | r = pytest.main(["-s", "tests/unit"]) 4 | if r: 5 | raise Exception("There were test failures or errors.") --------------------------------------------------------------------------------