From de240d26638bd5e001abc5c90b01437882c8ea88 Mon Sep 17 00:00:00 2001 From: Rob Harman Date: Sun, 17 Dec 2023 23:50:09 -0500 Subject: [PATCH 1/4] Separate getting full, basic and location data Addresses changes in how vehicle location data is provided after late 2023 vehicle software updates. Retains get_vehicle_data() as the method to get all vehicle data to prevent breaking changes. Updates version to 2.10.0 as it the initial fix for the location changes didn't increment to 2.9.0 Adds two additional endpoints to teslapy/endpoints.json: VEHICLE_BASIC_DATA: gets only basic vehicle data. VEHICLE_LOCATION_DATA: basic vehicle_state and location_data. Updates VEHICLE_DATA to get all endpoints. Adds two methods to the Vehicle Class to use the new endpoints: get_vehicle_basic_data(): Get only basic vehicle data. get_vehicle_location_data(): Get basic vehicle data, and location data. Adds two new flags to CLI: -B --basic: Get only basic vehicle data -G --location: Get current location (GPS) data Updates readme. --- README.md | 4 ++++ cli.py | 8 ++++++++ teslapy/__init__.py | 29 ++++++++++++++++++++++++----- teslapy/endpoints.json | 10 ++++++++++ 4 files changed, 46 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 3e92c2d..03727e8 100644 --- a/README.md +++ b/README.md @@ -69,6 +69,8 @@ The `Vehicle` class extends `dict` and stores vehicle data returned by the Owner | `decode_option()` | No | lookup option code description (read from *option_codes.json*) | | `option_code_list()` 1 | No | lists known descriptions of the vehicle option codes | | `get_vehicle_data()` | Yes | gets a rollup of all the data request endpoints plus vehicle config | +| `get_vehicle_basic_data()` | Yes | gets basic vehicle data | +| `get_vehicle_location_data()` | Yes | gets the basic and location data for the vehicle| | `get_nearby_charging_sites()` | Yes | lists nearby Tesla-operated charging stations | | `get_service_scheduling_data()` | No | retrieves next service appointment for this vehicle | | `get_charge_history()` 2 | No | lists vehicle charging history data points | @@ -455,6 +457,8 @@ optional arguments: -r, --stream receive streaming vehicle data on-change -S, --service get service self scheduling eligibility -H, --history get charging history data + -B, --basic get basic vehicle data only + -G, --location get location (GPS) data, wake as needed -V, --verify disable verify SSL certificate -L, --logout clear token from cache and logout -u, --user get user account details diff --git a/cli.py b/cli.py index a07f097..8e7b0c6 100755 --- a/cli.py +++ b/cli.py @@ -74,6 +74,10 @@ def main(): product.sync_wake_up() if args.get: print(product.get_vehicle_data()) + if args.location: + print(product.get_vehicle_location_data()) + if args.basic: + print(product.get_vehicle_basic_data()) if args.nearby: print(product.get_nearby_charging_sites()) if args.mobile: @@ -155,6 +159,10 @@ def main(): help='get service self scheduling eligibility') parser.add_argument('-H', '--history', action='store_true', help='get charging history data') + parser.add_argument('-B', '--basic', action='store_true', + help='get basic vhicle data only') + parser.add_argument('-G', '--location', action='store_true', + help='get location (GPS) data, wake as needed') parser.add_argument('-V', '--verify', action='store_false', help='disable verify SSL certificate') parser.add_argument('-L', '--logout', action='store_true', diff --git a/teslapy/__init__.py b/teslapy/__init__.py index 8b273ca..6855ee0 100644 --- a/teslapy/__init__.py +++ b/teslapy/__init__.py @@ -6,7 +6,7 @@ # Author: Tim Dorssers -__version__ = '2.8.0' +__version__ = '2.10.0' import os import ast @@ -541,11 +541,30 @@ def option_code_list(self): for code in codes.split(',')])) def get_vehicle_data(self): - """ A rollup of all the data request endpoints plus vehicle config. + """ Get vehicle data from all endpoints: location_data, charge_state, + climate_state, vehicle_state, vehicle_config, gui_settings. Raises + HTTPError when vehicle is not online. """ + self.update(self.api('VEHICLE_DATA')['response']) + self.timestamp = time.time() + return self + + def get_vehicle_basic_data(self): + """ Get only basic vehicle data, does not include location or config. Raises HTTPError when vehicle is not online. """ - self.update(self.api('VEHICLE_DATA', endpoints='location_data;' - 'charge_state;climate_state;vehicle_state;' - 'gui_settings;vehicle_config')['response']) + self.update(self.api('VEHICLE_BASIC_DATA')['response']) + self.timestamp = time.time() + return self + + def get_vehicle_location_data(self): + """ Get basic vehicle data and data from location_data endpoint. Wakes + vehicle if location data is not already present. Raises HTTPError when + vehicle is not online. """ + try: + self.get_vehicle_location_data()["drive_state"]["gps_as_of"] + except (KeyError): + self.sync_wake_up() + self.get_vehicle_location_data()["drive_state"] + self.update(self.api('VEHICLE_LOCATION_DATA')['response']) self.timestamp = time.time() return self diff --git a/teslapy/endpoints.json b/teslapy/endpoints.json index e113553..0aaf8cd 100644 --- a/teslapy/endpoints.json +++ b/teslapy/endpoints.json @@ -30,10 +30,20 @@ "AUTH": true }, "VEHICLE_DATA": { + "TYPE": "GET", + "URI": "api/1/vehicles/{vehicle_id}/vehicle_data?endpoints=location_data%3Bcharge_state%3Bclimate_state%3Bvehicle_state%3Bvehicle_config%3Bgui_settings", + "AUTH": true + }, + "VEHICLE_BASIC_DATA": { "TYPE": "GET", "URI": "api/1/vehicles/{vehicle_id}/vehicle_data", "AUTH": true }, + "VEHICLE_LOCATION_DATA": { + "TYPE": "GET", + "URI": "api/1/vehicles/{vehicle_id}/vehicle_data?endpoints=location_data", + "AUTH": true + }, "VEHICLE_SERVICE_DATA": { "TYPE": "GET", "URI": "api/1/vehicles/{vehicle_id}/service_data", From 47fc482d0bdcc4864ded5b899da6b20b54350024 Mon Sep 17 00:00:00 2001 From: Rob Harman Date: Mon, 18 Dec 2023 21:50:19 -0500 Subject: [PATCH 2/4] Fix missing version update in setup.cfg --- setup.cfg | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.cfg b/setup.cfg index 3b80f42..294e0f9 100644 --- a/setup.cfg +++ b/setup.cfg @@ -3,7 +3,7 @@ universal = 1 [metadata] name = TeslaPy -version = 2.8.0 +version = 2.10.0 author = Tim Dorssers author_email = tim.dorssers@xs4all.nl description = A Python module to use the Tesla Motors Owner API From 8ee7fe3ec1898fd21cd720b3b44992b773a84c56 Mon Sep 17 00:00:00 2001 From: Rob Harman Date: Fri, 22 Dec 2023 02:25:32 -0500 Subject: [PATCH 3/4] Revert endpoints, update __init__, set v=2.9.0 Implements feedback in original pull request: https://github.com/tdorssers/TeslaPy/pull/153#issuecomment-1863336305 Reverts endpoints.json to previous version for Android consistency: remove VEHICLE_BASIC_DATA remove VEHICLE_LOCATION_DATA Update __init__ to include endpoints in Vehicle Class methods - get_vehicle_data(): all endpoints - location_data, charge_state; climate_state, vehicle_state, gui_settings, vehicle_config. - get_vehicle_basic_data(): no extra endpints. - get_vehicle_location_data(): only location_data and basic data. Set version number to 2.9.0. --- setup.cfg | 2 +- teslapy/__init__.py | 16 +++++++++------- teslapy/endpoints.json | 10 ---------- 3 files changed, 10 insertions(+), 18 deletions(-) diff --git a/setup.cfg b/setup.cfg index 294e0f9..834451d 100644 --- a/setup.cfg +++ b/setup.cfg @@ -3,7 +3,7 @@ universal = 1 [metadata] name = TeslaPy -version = 2.10.0 +version = 2.9.0 author = Tim Dorssers author_email = tim.dorssers@xs4all.nl description = A Python module to use the Tesla Motors Owner API diff --git a/teslapy/__init__.py b/teslapy/__init__.py index 6855ee0..a7505da 100644 --- a/teslapy/__init__.py +++ b/teslapy/__init__.py @@ -6,7 +6,7 @@ # Author: Tim Dorssers -__version__ = '2.10.0' +__version__ = '2.9.0' import os import ast @@ -541,17 +541,18 @@ def option_code_list(self): for code in codes.split(',')])) def get_vehicle_data(self): - """ Get vehicle data from all endpoints: location_data, charge_state, - climate_state, vehicle_state, vehicle_config, gui_settings. Raises - HTTPError when vehicle is not online. """ - self.update(self.api('VEHICLE_DATA')['response']) + """ A rollup of all the data request endpoints plus vehicle config. + Raises HTTPError when vehicle is not online. """ + self.update(self.api('VEHICLE_DATA', endpoints='location_data;' + 'charge_state;climate_state;vehicle_state;' + 'gui_settings;vehicle_config')['response']) self.timestamp = time.time() return self def get_vehicle_basic_data(self): """ Get only basic vehicle data, does not include location or config. Raises HTTPError when vehicle is not online. """ - self.update(self.api('VEHICLE_BASIC_DATA')['response']) + self.update(self.api('VEHICLE_DATA')['response']) self.timestamp = time.time() return self @@ -564,7 +565,8 @@ def get_vehicle_location_data(self): except (KeyError): self.sync_wake_up() self.get_vehicle_location_data()["drive_state"] - self.update(self.api('VEHICLE_LOCATION_DATA')['response']) + self.update(self.api('VEHICLE_DATA', + endpoints='location_data')['response']) self.timestamp = time.time() return self diff --git a/teslapy/endpoints.json b/teslapy/endpoints.json index 0aaf8cd..e113553 100644 --- a/teslapy/endpoints.json +++ b/teslapy/endpoints.json @@ -30,20 +30,10 @@ "AUTH": true }, "VEHICLE_DATA": { - "TYPE": "GET", - "URI": "api/1/vehicles/{vehicle_id}/vehicle_data?endpoints=location_data%3Bcharge_state%3Bclimate_state%3Bvehicle_state%3Bvehicle_config%3Bgui_settings", - "AUTH": true - }, - "VEHICLE_BASIC_DATA": { "TYPE": "GET", "URI": "api/1/vehicles/{vehicle_id}/vehicle_data", "AUTH": true }, - "VEHICLE_LOCATION_DATA": { - "TYPE": "GET", - "URI": "api/1/vehicles/{vehicle_id}/vehicle_data?endpoints=location_data", - "AUTH": true - }, "VEHICLE_SERVICE_DATA": { "TYPE": "GET", "URI": "api/1/vehicles/{vehicle_id}/service_data", From 0325e581855b08be3aa7729d86be56fc22338866 Mon Sep 17 00:00:00 2001 From: Rob Harman Date: Fri, 22 Dec 2023 15:15:51 -0500 Subject: [PATCH 4/4] Get recent location data, cleanup endpoint requests Updates Vehicle Class: Removes get_vehicle_basic_data() Updates get_vehicle_data() to allow selection of individual endpoints, defaults to all endpoints. Updates get_vehicle_location_data(): - Adds optional location data max_age in seconds. Defaults to 300 (five minutes). - Updates and cleans up logic to prevent issues with missing data if it ages out in persistent connections. - Fix recursion error when vehicle was online but doesn't come back online for subsequent requests. Minor fixes: - Swaps most string formatting with fStrings, not panel ones since I can't test panel changes without a panel, and not the conversion ones since fStrings are less readable there. - Reflows lines over 80 chars, with same panel exception, most significantly in Vechicle.decode_vin() - Minor linting/spacing changes --- README.md | 3 +- cli.py | 16 +++-- teslapy/__init__.py | 151 +++++++++++++++++++++++--------------------- 3 files changed, 90 insertions(+), 80 deletions(-) diff --git a/README.md b/README.md index 03727e8..0f20421 100644 --- a/README.md +++ b/README.md @@ -68,8 +68,7 @@ The `Vehicle` class extends `dict` and stores vehicle data returned by the Owner | `sync_wake_up()` | No | wakes up and waits for the vehicle to come online | | `decode_option()` | No | lookup option code description (read from *option_codes.json*) | | `option_code_list()` 1 | No | lists known descriptions of the vehicle option codes | -| `get_vehicle_data()` | Yes | gets a rollup of all the data request endpoints plus vehicle config | -| `get_vehicle_basic_data()` | Yes | gets basic vehicle data | +| `get_vehicle_data()` | Yes | get vehicle data for selected endpoints, defaults to all endpoints| | `get_vehicle_location_data()` | Yes | gets the basic and location data for the vehicle| | `get_nearby_charging_sites()` | Yes | lists nearby Tesla-operated charging stations | | `get_service_scheduling_data()` | No | retrieves next service appointment for this vehicle | diff --git a/cli.py b/cli.py index 8e7b0c6..3a70622 100755 --- a/cli.py +++ b/cli.py @@ -20,11 +20,13 @@ raw_input = vars(__builtins__).get('raw_input', input) # Py2/3 compatibility + def custom_auth(url): - # Use pywebview if no web browser specified + """ Use pywebview if no web browser specified """ if webview and not (webdriver and args.web is not None): result = [''] window = webview.create_window('Login', url) + def on_loaded(): result[0] = window.get_current_url() if 'void/callback' in result[0].split('?')[0]: @@ -46,6 +48,7 @@ def on_loaded(): WebDriverWait(browser, 300).until(EC.url_contains('void/callback')) return browser.current_url + def main(): default_format = '%(asctime)s - %(name)s - %(levelname)s - %(message)s' logging.basicConfig(level=logging.DEBUG if args.debug else logging.INFO, @@ -63,7 +66,7 @@ def main(): selected = [p for p in prod for v in p.values() if v == args.filter] logging.info('%d product(s), %d selected', len(prod), len(selected)) for i, product in enumerate(selected): - print('Product %d:' % i) + print(f'Product {i}:') # Show information or invoke API depending on arguments if args.list: print(product) @@ -77,7 +80,7 @@ def main(): if args.location: print(product.get_vehicle_location_data()) if args.basic: - print(product.get_vehicle_basic_data()) + print(product.get_vehicle_data(endpoints='')) if args.nearby: print(product.get_nearby_charging_sites()) if args.mobile: @@ -121,6 +124,7 @@ def main(): else: tesla.logout(not (webdriver and args.web is not None)) + if __name__ == "__main__": parser = argparse.ArgumentParser(description='Tesla Owner API CLI') parser.add_argument('-e', dest='email', help='login email', required=True) @@ -168,11 +172,11 @@ def main(): parser.add_argument('-L', '--logout', action='store_true', help='clear token from cache and logout') if webdriver: - h = 'use Chrome browser' if webview else 'use Chrome browser (default)' + H = 'use Chrome browser' if webview else 'use Chrome browser (default)' parser.add_argument('--chrome', action='store_const', dest='web', - help=h, const=0, default=None if webview else 0) + help=H, const=0, default=None if webview else 0) parser.add_argument('--opera', action='store_const', dest='web', - help='use Opera browser', const=1) + help='use Opera browser', const=1) if hasattr(webdriver.edge, 'options'): parser.add_argument('--edge', action='store_const', dest='web', help='use Edge browser', const=2) diff --git a/teslapy/__init__.py b/teslapy/__init__.py index a7505da..b3fb892 100644 --- a/teslapy/__init__.py +++ b/teslapy/__init__.py @@ -192,7 +192,8 @@ def authorization_url(self, url='oauth2/v3/authorize', url = urljoin(self.sso_base_url, url) kwargs['code_challenge'] = code_challenge kwargs['code_challenge_method'] = 'S256' - without_hint, state = super(Tesla, self).authorization_url(url, **kwargs) + without_hint, state = super(Tesla, self).authorization_url(url, + **kwargs) # Detect account's registered region kwargs['login_hint'] = self.email kwargs['state'] = state @@ -291,9 +292,9 @@ def _authenticate(url): def _cache_load(self): """ Default cache loader method """ try: - with open(self.cache_file) as infile: + with open(self.cache_file, encoding='utf-8') as infile: cache = json.load(infile) - except (IOError, ValueError) as e: + except (IOError, ValueError): logger.warning('Cannot load cache: %s', self.cache_file, exc_info=True) cache = {} @@ -302,9 +303,11 @@ def _cache_load(self): def _cache_dump(self, cache): """ Default cache dumper method """ try: - with open(self.cache_file, 'w') as outfile: + with open(self.cache_file, 'w', encoding='utf-8') as outfile: json.dump(cache, outfile) - os.chmod(self.cache_file, (stat.S_IWUSR | stat.S_IRUSR | stat.S_IRGRP)) + os.chmod(self.cache_file, + (stat.S_IWUSR | stat.S_IRUSR | stat.S_IRGRP) + ) except IOError: logger.error('Cache not updated') else: @@ -353,8 +356,8 @@ def api(self, name, path_vars=None, **kwargs): # Lookup endpoint name try: endpoint = self.endpoints[name] - except KeyError: - raise ValueError('Unknown endpoint name ' + name) + except KeyError as e: + raise ValueError(f'Unknown endpoint name {name}') from e # Fetch token if not authorized and API requires authorization if endpoint['AUTH'] and not self.authorized: self.fetch_token() @@ -362,7 +365,7 @@ def api(self, name, path_vars=None, **kwargs): try: uri = endpoint['URI'].format(**path_vars) except KeyError as e: - raise ValueError('%s requires path variable %s' % (name, e)) + raise ValueError(f"{name} requires path variable {e}") from e # Perform request using given keyword arguments as parameters arg_name = 'params' if endpoint['TYPE'] == 'GET' else 'json' serialize = endpoint.get('CONTENT') != 'HTML' and name != 'STATUS' @@ -514,8 +517,9 @@ def sync_wake_up(self, timeout=60, interval=2, backoff=1.15): break # Raise exception when task has timed out if start_time + timeout - interval < time.time(): - raise VehicleError('%s not woken up within %s seconds' - % (self['display_name'], timeout)) + name = self['display_name'] + err = f"{name} not woken up within {timeout} seconds" + raise VehicleError(err) interval *= backoff logger.info('%s is %s', self['display_name'], self['state']) @@ -540,33 +544,31 @@ def option_code_list(self): return list(filter(None, [self.decode_option(code) for code in codes.split(',')])) - def get_vehicle_data(self): - """ A rollup of all the data request endpoints plus vehicle config. - Raises HTTPError when vehicle is not online. """ - self.update(self.api('VEHICLE_DATA', endpoints='location_data;' - 'charge_state;climate_state;vehicle_state;' - 'gui_settings;vehicle_config')['response']) - self.timestamp = time.time() - return self + def get_vehicle_data(self, endpoints='location_data;charge_state;' + 'climate_state;vehicle_state;' + 'gui_settings;vehicle_config'): + """ Allow specifying individual endpoints to query. Defaults to all + endpoints. Raises HTTPError when vehicle is not online. - def get_vehicle_basic_data(self): - """ Get only basic vehicle data, does not include location or config. - Raises HTTPError when vehicle is not online. """ - self.update(self.api('VEHICLE_DATA')['response']) + endpoints: string containing each endpoint to query, separate with ;""" + self.update(self.api('VEHICLE_DATA', endpoints=endpoints)['response']) self.timestamp = time.time() return self - def get_vehicle_location_data(self): - """ Get basic vehicle data and data from location_data endpoint. Wakes - vehicle if location data is not already present. Raises HTTPError when - vehicle is not online. """ - try: - self.get_vehicle_location_data()["drive_state"]["gps_as_of"] - except (KeyError): + def get_vehicle_location_data(self, max_age=300): + """ Get basic and location_data. Wakes vehicle if location data is not + already present, or older than max_age seconds. Raises HTTPError when + vehicle is not online. + + max_age: how long in seconds before refreshing location data. Defaults + to 300 (5 minutes). """ + last_update = self.get('drive_state', {}).get('gps_as_of') + # Check for cached data more recent than max_age + if last_update is None or last_update < (time.time() - max_age): self.sync_wake_up() - self.get_vehicle_location_data()["drive_state"] - self.update(self.api('VEHICLE_DATA', - endpoints='location_data')['response']) + self.update(self.api('VEHICLE_DATA', + endpoints='location_data')['response']) + self.timestamp = time.time() self.timestamp = time.time() return self @@ -589,7 +591,7 @@ def mobile_enabled(self): """ Checks if the Mobile Access setting is enabled in the car. Raises HTTPError when vehicle is in service or not online. """ # Construct URL and send request - uri = 'api/1/vehicles/%s/mobile_enabled' % self['id_s'] + uri = f"api/1/vehicles/{self['id_s']}/mobile_enabled" return self.tesla.get(uri)['response'] def compose_image(self, view='STUD_3QTR', size=640, options=None): @@ -604,7 +606,7 @@ def compose_image(self, view='STUD_3QTR', size=640, options=None): # Retrieve image from compositor url = 'https://static-assets.tesla.com/v1/compositor/' response = requests.get(url, params=params, verify=self.tesla.verify, - proxies=self.tesla.proxies) + proxies=self.tesla.proxies, timeout=30) response.raise_for_status() # Raise HTTPError, if one occurred return response.content @@ -657,37 +659,41 @@ def last_seen(self): def decode_vin(self): """ Returns decoded VIN as dict """ make = 'Tesla Model ' + self['vin'][3] - body = {'A': 'Hatch back 5 Dr / LHD', 'B': 'Hatch back 5 Dr / RHD', - 'C': 'Class E MPV / 5 Dr / LHD', 'E': 'Sedan 4 Dr / LHD', - 'D': 'Class E MPV / 5 Dr / RHD', 'F': 'Sedan 4 Dr / RHD', - 'G': 'Class D MPV / 5 Dr / LHD', 'H': 'Class D MPV / 5 Dr / RHD' - }.get(self['vin'][4], 'Unknown') - belt = {'1': 'Type 2 manual seatbelts (FR, SR*3) with front airbags, ' - 'PODS, side inflatable restraints, knee airbags (FR)', - '3': 'Type 2 manual seatbelts (FR, SR*2) with front airbags, ' - 'side inflatable restraints, knee airbags (FR)', - '4': 'Type 2 manual seatbelts (FR, SR*2) with front airbags, ' - 'side inflatable restraints, knee airbags (FR)', - '5': 'Type 2 manual seatbelts (FR, SR*2) with front airbags, ' - 'side inflatable restraints', - '6': 'Type 2 manual seatbelts (FR, SR*3) with front airbags, ' - 'side inflatable restraints', - '7': 'Type 2 manual seatbelts (FR, SR*3) with front airbags, ' - 'side inflatable restraints & active hood', - '8': 'Type 2 manual seatbelts (FR, SR*2) with front airbags, ' - 'side inflatable restraints & active hood', - 'A': 'Type 2 manual seatbelts (FR, SR*3, TR*2) with front ' - 'airbags, PODS, side inflatable restraints, knee airbags (FR)', - 'B': 'Type 2 manual seatbelts (FR, SR*2, TR*2) with front ' - 'airbags, PODS, side inflatable restraints, knee airbags (FR)', - 'C': 'Type 2 manual seatbelts (FR, SR*2, TR*2) with front ' - 'airbags, PODS, side inflatable restraints, knee airbags (FR)', - 'D': 'Type 2 Manual Seatbelts (FR, SR*3) with front airbag, ' - 'PODS, side inflatable restraints, knee airbags (FR)' - }.get(self['vin'][5], 'Unknown') - batt = {'E': 'Electric (NMC)', 'F': 'Li-Phosphate (LFP)', - 'H': 'High Capacity (NMC)', 'S': 'Standard (NMC)', - 'V': 'Ultra Capacity (NMC)'}.get(self['vin'][6], 'Unknown') + body = { + 'A': 'Hatch back 5 Dr / LHD', 'B': 'Hatch back 5 Dr / RHD', + 'C': 'Class E MPV / 5 Dr / LHD', 'E': 'Sedan 4 Dr / LHD', + 'D': 'Class E MPV / 5 Dr / RHD', 'F': 'Sedan 4 Dr / RHD', + 'G': 'Class D MPV / 5 Dr / LHD', 'H': 'Class D MPV / 5 Dr / RHD' + }.get(self['vin'][4], 'Unknown') + belt = { + '1': 'Type 2 manual seatbelts (FR, SR*3) with front airbags, ' + 'PODS, side inflatable restraints, knee airbags (FR)', + '3': 'Type 2 manual seatbelts (FR, SR*2) with front airbags, ' + 'side inflatable restraints, knee airbags (FR)', + '4': 'Type 2 manual seatbelts (FR, SR*2) with front airbags, ' + 'side inflatable restraints, knee airbags (FR)', + '5': 'Type 2 manual seatbelts (FR, SR*2) with front airbags, ' + 'side inflatable restraints', + '6': 'Type 2 manual seatbelts (FR, SR*3) with front airbags, ' + 'side inflatable restraints', + '7': 'Type 2 manual seatbelts (FR, SR*3) with front airbags, ' + 'side inflatable restraints & active hood', + '8': 'Type 2 manual seatbelts (FR, SR*2) with front airbags, ' + 'side inflatable restraints & active hood', + 'A': 'Type 2 manual seatbelts (FR, SR*3, TR*2) with front ' + 'airbags, PODS, side inflatable restraints, knee airbags (FR)', + 'B': 'Type 2 manual seatbelts (FR, SR*2, TR*2) with front ' + 'airbags, PODS, side inflatable restraints, knee airbags (FR)', + 'C': 'Type 2 manual seatbelts (FR, SR*2, TR*2) with front ' + 'airbags, PODS, side inflatable restraints, knee airbags (FR)', + 'D': 'Type 2 Manual Seatbelts (FR, SR*3) with front airbag, ' + 'PODS, side inflatable restraints, knee airbags (FR)' + }.get(self['vin'][5], 'Unknown') + batt = { + 'E': 'Electric (NMC)', 'F': 'Li-Phosphate (LFP)', + 'H': 'High Capacity (NMC)', 'S': 'Standard (NMC)', + 'V': 'Ultra Capacity (NMC)' + }.get(self['vin'][6], 'Unknown') drive = {'1': 'Single Motor - Standard', '2': 'Dual Motor - Standard', '3': 'Single Motor - Performance', '5': 'P2 Dual Motor', '4': 'Dual Motor - Performance', '6': 'P2 Tri Motor', @@ -799,8 +805,8 @@ class BatteryTariffPeriodCost( """ Represents the costs of a tariff period buy: A float containing the import price sell: A float containing the export price - name: The name for the period, must be 'ON_PEAK', 'PARTIAL_PEAK', 'OFF_PEAK', - or 'SUPER_OFF_PEAK' + name: The name for the period, must be 'ON_PEAK', 'PARTIAL_PEAK', + 'OFF_PEAK', or 'SUPER_OFF_PEAK' """ __slots__ = () @@ -881,8 +887,8 @@ def create_tariff(default_price, periods, provider, plan): if bg_period[0] <= period.start and period.end <= bg_period[1]: slot_found = True # If the period matches the start/end times, then we just - # need to adjust the existing background time slot. Otherwise - # we need to split it. + # need to adjust the existing background time slot. + # Otherwise we need to split it. if bg_period[0] == period.start: background_time[index][0] = period.end elif bg_period[1] == period.end: @@ -896,9 +902,10 @@ def create_tariff(default_price, periods, provider, plan): costs[period.cost].append(period) # The loop above can leave background time slots with zero duration. - # It's difficult to filter them out above as the list indexes can get - # out of sync as we end up modifying the array being iterated over. - # As a result it's easier to filter out invalid background slots now. + # It's difficult to filter them out above as the list indexes can + # get out of sync as we end up modifying the array being iterated + # over. As a result it's easier to filter out invalid background + # slots now. background_time = list(filter(lambda t: t[0] != t[1], background_time))