From 7b5ddc51b14da341a7a34220ec2d4a56384cbdb9 Mon Sep 17 00:00:00 2001 From: Sergey Isachenko Date: Mon, 28 Jan 2019 14:40:20 +0300 Subject: [PATCH 01/13] possible fix for https://github.com/zabuldon/teslajsonpy/issues/16 --- setup.py | 4 +++- teslajsonpy/Exceptions.py | 3 +++ teslajsonpy/connection.py | 1 + teslajsonpy/controller.py | 45 +++++++++++++++++++++++++++++++++++---- 4 files changed, 48 insertions(+), 5 deletions(-) diff --git a/setup.py b/setup.py index 43d6b97f..a2db877b 100644 --- a/setup.py +++ b/setup.py @@ -1,7 +1,7 @@ from setuptools import setup setup( name='teslajsonpy', - version='0.0.22', + version='0.0.24', packages=['teslajsonpy'], include_package_data=True, python_requires='>=3', @@ -17,6 +17,8 @@ 'Programming Language :: Python :: 3', 'Programming Language :: Python :: 3.4', 'Programming Language :: Python :: 3.5', + 'Programming Language :: Python :: 3.6', + 'Programming Language :: Python :: 3.7', 'Topic :: Internet', ], ) diff --git a/teslajsonpy/Exceptions.py b/teslajsonpy/Exceptions.py index 01966839..28dbc7d0 100644 --- a/teslajsonpy/Exceptions.py +++ b/teslajsonpy/Exceptions.py @@ -19,3 +19,6 @@ def __init__(self, code, *args, **kwargs): self.message = 'SERVICE_MAINTENANCE' elif self.code > 299: self.message = "UNKNOWN_ERROR" + +class RetryLimitError(TeslaException): + pass \ No newline at end of file diff --git a/teslajsonpy/connection.py b/teslajsonpy/connection.py index b9e36458..e857bec7 100644 --- a/teslajsonpy/connection.py +++ b/teslajsonpy/connection.py @@ -9,6 +9,7 @@ class Connection(object): """Connection to Tesla Motors API""" + def __init__(self, email, password): """Initialize connection object""" self.user_agent = 'Model S 2.1.79 (SM-G900V; Android REL 4.4.4; en_US' diff --git a/teslajsonpy/controller.py b/teslajsonpy/controller.py index 23edf0f0..3378a2a2 100644 --- a/teslajsonpy/controller.py +++ b/teslajsonpy/controller.py @@ -7,6 +7,8 @@ from teslajsonpy.BinarySensor import ParkingSensor, ChargerConnectionSensor from teslajsonpy.Charger import ChargerSwitch, RangeSwitch from teslajsonpy.GPS import GPS, Odometer +from functools import wraps +from .Exceptions import RetryLimitError class Controller: @@ -21,11 +23,17 @@ def __init__(self, email, password, update_interval): self.__driving = {} self.__gui = {} self.__last_update_time = {} + self.__last_wake_up_time = {} self.__lock = RLock() + self.__car_online = {} + cars = self.__connection.get('vehicles')['response'] + for car in cars: self.__last_update_time[car['id']] = 0 + self.__last_wake_up_time[car['id']] = 0 self.__update[car['id']] = True + self.__car_online[car['id']] = False self.update(car['id']) self.__vehicles.append(Climate(car, self)) self.__vehicles.append(Battery(car, self)) @@ -55,15 +63,37 @@ def command(self, vehicle_id, name, data={}): def list_vehicles(self): return self.__vehicles - def wake_up(self, vehicle_id): - self.post(vehicle_id, 'wake_up') - + def wake_up(f): + @wraps(f) + def wrapped(inst, *args, **kwargs): + retries = 0 + sleep_delay = 2 + while True: + result = inst._wake_up(*args) + if not result: + if retries < 5: + time.sleep(sleep_delay) + continue + else: + raise RetryLimitError + else: + break + return f(inst, *args, **kwargs) + return wrapped + + def _wake_up(self, vehicle_id): + cur_time = int(time.time()) + if not self.__car_online[vehicle_id] or cur_time - self.__last_wake_up_time[vehicle_id] > 300: + self.__car_online[vehicle_id] = self.post(vehicle_id, 'wake_up')['response']['state'] == 'online' + self.__last_wake_up_time[vehicle_id] = cur_time + return self.__car_online[vehicle_id] + + @wake_up def update(self, car_id): cur_time = time.time() with self.__lock: if (self.__update[car_id] and (cur_time - self.__last_update_time[car_id] > self.update_interval)): - self.wake_up(car_id) data = self.get(car_id, 'data') if data and data['response']: self.__climate[car_id] = data['response']['climate_state'] @@ -79,26 +109,33 @@ def update(self, car_id): self.__driving[car_id] = False self.__gui[car_id] = False + @wake_up def get_climate_params(self, car_id): return self.__climate[car_id] + @wake_up def get_charging_params(self, car_id): return self.__charging[car_id] + @wake_up def get_state_params(self, car_id): return self.__state[car_id] + @wake_up def get_drive_params(self, car_id): return self.__driving[car_id] + @wake_up def get_gui_params(self, car_id): return self.__gui[car_id] + @wake_up def get_updates(self, car_id=None): if car_id is not None: return self.__update[car_id] else: return self.__update + @wake_up def set_updates(self, car_id, value): self.__update[car_id] = value From b97ed2435495eecc01ae4152688433c53aac9a97 Mon Sep 17 00:00:00 2001 From: Sergey Isachenko Date: Tue, 5 Feb 2019 10:18:19 +0300 Subject: [PATCH 02/13] Add increment to retries. --- teslajsonpy/controller.py | 1 + 1 file changed, 1 insertion(+) diff --git a/teslajsonpy/controller.py b/teslajsonpy/controller.py index 3378a2a2..b934ab41 100644 --- a/teslajsonpy/controller.py +++ b/teslajsonpy/controller.py @@ -73,6 +73,7 @@ def wrapped(inst, *args, **kwargs): if not result: if retries < 5: time.sleep(sleep_delay) + retries += 1 continue else: raise RetryLimitError From 243d450493d3c3ec151857a111024265fda43d5c Mon Sep 17 00:00:00 2001 From: "Alan D. Tse" Date: Mon, 11 Feb 2019 00:13:37 -0800 Subject: [PATCH 03/13] Update wake_up wrapper with wake_if_asleep flag --- teslajsonpy/BatterySensor.py | 4 +- teslajsonpy/BinarySensor.py | 4 +- teslajsonpy/Charger.py | 16 +++-- teslajsonpy/Climate.py | 17 ++++-- teslajsonpy/Exceptions.py | 3 +- teslajsonpy/GPS.py | 4 +- teslajsonpy/Lock.py | 16 +++-- teslajsonpy/controller.py | 114 ++++++++++++++++++++++++----------- 8 files changed, 119 insertions(+), 59 deletions(-) diff --git a/teslajsonpy/BatterySensor.py b/teslajsonpy/BatterySensor.py index 29cfc112..6106e452 100644 --- a/teslajsonpy/BatterySensor.py +++ b/teslajsonpy/BatterySensor.py @@ -16,7 +16,7 @@ def __init__(self, data, controller): self.update() def update(self): - self._controller.update(self._id) + self._controller.update(self._id, wake_if_asleep=False) data = self._controller.get_charging_params(self._id) if data: self.__battery_level = data['battery_level'] @@ -46,7 +46,7 @@ def __init__(self, data, controller): self.update() def update(self): - self._controller.update(self._id) + self._controller.update(self._id, wake_if_asleep=False) data = self._controller.get_charging_params(self._id) if data: self.__battery_range = data['battery_range'] diff --git a/teslajsonpy/BinarySensor.py b/teslajsonpy/BinarySensor.py index 163ebf03..ab1bddf0 100644 --- a/teslajsonpy/BinarySensor.py +++ b/teslajsonpy/BinarySensor.py @@ -16,7 +16,7 @@ def __init__(self, data, controller): self.update() def update(self): - self._controller.update(self._id) + self._controller.update(self._id, wake_if_asleep=False) data = self._controller.get_drive_params(self._id) if data: if not data['shift_state'] or data['shift_state'] == 'P': @@ -45,7 +45,7 @@ def __init__(self, data, controller): self.bin_type = 0x2 def update(self): - self._controller.update(self._id) + self._controller.update(self._id, wake_if_asleep=False) data = self._controller.get_charging_params(self._id) if data: if data['charging_state'] in ["Disconnected", "Stopped", "NoPower"]: diff --git a/teslajsonpy/Charger.py b/teslajsonpy/Charger.py index 37d11d3c..e8eba65e 100644 --- a/teslajsonpy/Charger.py +++ b/teslajsonpy/Charger.py @@ -15,7 +15,7 @@ def __init__(self, data, controller): self.update() def update(self): - self._controller.update(self._id) + self._controller.update(self._id, wake_if_asleep=False) data = self._controller.get_charging_params(self._id) if data and (time.time() - self.__manual_update_time > 60): if data['charging_state'] != "Charging": @@ -25,14 +25,16 @@ def update(self): def start_charge(self): if not self.__charger_state: - data = self._controller.command(self._id, 'charge_start') + data = self._controller.command(self._id, 'charge_start', + wake_if_asleep=True) if data and data['response']['result']: self.__charger_state = True self.__manual_update_time = time.time() def stop_charge(self): if self.__charger_state: - data = self._controller.command(self._id, 'charge_stop') + data = self._controller.command(self._id, 'charge_stop', + wake_if_asleep=True) if data and data['response']['result']: self.__charger_state = False self.__manual_update_time = time.time() @@ -58,21 +60,23 @@ def __init__(self, data, controller): self.update() def update(self): - self._controller.update(self._id) + self._controller.update(self._id, wake_if_asleep=False) data = self._controller.get_charging_params(self._id) if data and (time.time() - self.__manual_update_time > 60): self.__maxrange_state = data['charge_to_max_range'] def set_max(self): if not self.__maxrange_state: - data = self._controller.command(self._id, 'charge_max_range') + data = self._controller.command(self._id, 'charge_max_range', + wake_if_asleep=True) if data['response']['result']: self.__maxrange_state = True self.__manual_update_time = time.time() def set_standard(self): if self.__maxrange_state: - data = self._controller.command(self._id, 'charge_standard') + data = self._controller.command(self._id, 'charge_standard', + wake_if_asleep=True) if data and data['response']['result']: self.__maxrange_state = False self.__manual_update_time = time.time() diff --git a/teslajsonpy/Climate.py b/teslajsonpy/Climate.py index 0b30b5c3..4757e1fa 100644 --- a/teslajsonpy/Climate.py +++ b/teslajsonpy/Climate.py @@ -38,7 +38,7 @@ def get_fan_status(self): return self.__fan_status def update(self): - self._controller.update(self._id) + self._controller.update(self._id, wake_if_asleep=False) data = self._controller.get_climate_params(self._id) if data: @@ -56,7 +56,10 @@ def update(self): def set_temperature(self, temp): temp = round(temp, 1) self.__manual_update_time = time.time() - data = self._controller.command(self._id, 'set_temps', {"driver_temp": temp, "passenger_temp": temp}) + data = self._controller.command(self._id, 'set_temps', + {"driver_temp": temp, + "passenger_temp": temp}, + wake_if_asleep=True) if data['response']['result']: self.__driver_temp_setting = temp self.__passenger_temp_setting = temp @@ -64,12 +67,16 @@ def set_temperature(self, temp): def set_status(self, enabled): self.__manual_update_time = time.time() if enabled: - data = self._controller.command(self._id, 'auto_conditioning_start') + data = self._controller.command(self._id, + 'auto_conditioning_start', + wake_if_asleep=True) if data['response']['result']: self.__is_auto_conditioning_on = True self.__is_climate_on = True else: - data = self._controller.command(self._id, 'auto_conditioning_stop') + data = self._controller.command(self._id, + 'auto_conditioning_stop', + wake_if_asleep=True) if data['response']['result']: self.__is_auto_conditioning_on = False self.__is_climate_on = False @@ -101,7 +108,7 @@ def get_outside_temp(self): return self.__outside_temp def update(self): - self._controller.update(self._id) + self._controller.update(self._id, wake_if_asleep=False) data = self._controller.get_climate_params(self._id) if data: self.__inside_temp = data['inside_temp'] if data['inside_temp'] else self.__inside_temp diff --git a/teslajsonpy/Exceptions.py b/teslajsonpy/Exceptions.py index 28dbc7d0..082fbe44 100644 --- a/teslajsonpy/Exceptions.py +++ b/teslajsonpy/Exceptions.py @@ -21,4 +21,5 @@ def __init__(self, code, *args, **kwargs): self.message = "UNKNOWN_ERROR" class RetryLimitError(TeslaException): - pass \ No newline at end of file + def __init__(self, *args, **kwargs): + pass diff --git a/teslajsonpy/GPS.py b/teslajsonpy/GPS.py index 1b7bbfb5..24c6b940 100644 --- a/teslajsonpy/GPS.py +++ b/teslajsonpy/GPS.py @@ -24,7 +24,7 @@ def get_location(self): return self.__location def update(self): - self._controller.update(self._id) + self._controller.update(self._id, wake_if_asleep=False) data = self._controller.get_drive_params(self._id) if data: self.__longitude = data['longitude'] @@ -54,7 +54,7 @@ def __init__(self, data, controller): self.__rated = True def update(self): - self._controller.update(self._id) + self._controller.update(self._id, wake_if_asleep=False) data = self._controller.get_state_params(self._id) if data: self.__odometer = data['odometer'] diff --git a/teslajsonpy/Lock.py b/teslajsonpy/Lock.py index 0de1e9db..cc27e26b 100644 --- a/teslajsonpy/Lock.py +++ b/teslajsonpy/Lock.py @@ -18,21 +18,23 @@ def __init__(self, data, controller): self.update() def update(self): - self._controller.update(self._id) + self._controller.update(self._id, wake_if_asleep=False) data = self._controller.get_state_params(self._id) if data and (time.time() - self.__manual_update_time > 60): self.__lock_state = data['locked'] def lock(self): if not self.__lock_state: - data = self._controller.command(self._id, 'door_lock') + data = self._controller.command(self._id, 'door_lock', + wake_if_asleep=True) if data['response']['result']: self.__lock_state = True self.__manual_update_time = time.time() def unlock(self): if self.__lock_state: - data = self._controller.command(self._id, 'door_unlock') + data = self._controller.command(self._id, 'door_unlock', + wake_if_asleep=True) if data['response']['result']: self.__lock_state = False self.__manual_update_time = time.time() @@ -61,21 +63,23 @@ def __init__(self, data, controller): self.update() def update(self): - self._controller.update(self._id) + self._controller.update(self._id, wake_if_asleep=False) data = self._controller.get_charging_params(self._id) if data and (time.time() - self.__manual_update_time > 60): self.__lock_state = not ((data['charge_port_door_open']) and (data['charge_port_door_open']) and (data['charge_port_latch'] != 'Engaged')) def lock(self): if not self.__lock_state: - data = self._controller.command(self._id, 'charge_port_door_close') + data = self._controller.command(self._id, 'charge_port_door_close', + wake_if_asleep=True) if data['response']['result']: self.__lock_state = True self.__manual_update_time = time.time() def unlock(self): if self.__lock_state: - data = self._controller.command(self._id, 'charge_port_door_open') + data = self._controller.command(self._id, 'charge_port_door_open', + wake_if_asleep=True) if data['response']['result']: self.__lock_state = False self.__manual_update_time = time.time() diff --git a/teslajsonpy/controller.py b/teslajsonpy/controller.py index b934ab41..b46ea80b 100644 --- a/teslajsonpy/controller.py +++ b/teslajsonpy/controller.py @@ -7,8 +7,11 @@ from teslajsonpy.BinarySensor import ParkingSensor, ChargerConnectionSensor from teslajsonpy.Charger import ChargerSwitch, RangeSwitch from teslajsonpy.GPS import GPS, Odometer +from teslajsonpy.Exceptions import TeslaException from functools import wraps from .Exceptions import RetryLimitError +import logging +_LOGGER = logging.getLogger(__name__) class Controller: @@ -34,7 +37,10 @@ def __init__(self, email, password, update_interval): self.__last_wake_up_time[car['id']] = 0 self.__update[car['id']] = True self.__car_online[car['id']] = False - self.update(car['id']) + try: + self.update(car['id'], wake_if_asleep=False) + except (TeslaException, RetryLimitError): + pass self.__vehicles.append(Climate(car, self)) self.__vehicles.append(Battery(car, self)) self.__vehicles.append(Range(car, self)) @@ -48,53 +54,95 @@ def __init__(self, email, password, update_interval): self.__vehicles.append(GPS(car, self)) self.__vehicles.append(Odometer(car, self)) + def wake_up(f): + """Wraps a API f so it will attempt to wake the vehicle if asleep. + + The command f is run once if the vehicle_id was last reported + online. Assuming f returns None and wake_if_asleep is True, 5 attempts + will be made to wake the vehicle to reissue the command. + Args: + inst (Controller): The instance of a controller + vehicle_id (string): The vehicle to attempt to wake. + wake_if_asleep (bool): Keyword arg to force a vehicle awake. Must be + set in the wrapped function f + Throws: + RetryLimitError + """ + @wraps(f) + def wrapped(*args, **kwargs): + retries = 0 + sleep_delay = 2 + inst = args[0] + vehicle_id = args[1] + result = None + if (vehicle_id is not None and vehicle_id in inst.__car_online and + inst.__car_online[vehicle_id]): + try: + result = f(*args, **kwargs) + except (TeslaException): + pass + if result is not None: + return result + else: + _LOGGER.debug("Wrapper: f:%s, result:%s, args:%s, kwargs:%s, \ + inst:%s, vehicle_id:%s, __cars_online:%s" % + (f, result, args, kwargs, inst, vehicle_id, + inst.__car_online)) + while ('wake_if_asleep' in kwargs and kwargs['wake_if_asleep'] + and + (vehicle_id is None or + (vehicle_id is not None and + vehicle_id in inst.__car_online and + not inst.__car_online[vehicle_id]))): + result = inst._wake_up(vehicle_id, *args, **kwargs) + _LOGGER.debug("Result(%s): %s" % (retries, result)) + if not result: + if retries < 5: + time.sleep(sleep_delay) + retries += 1 + continue + else: + raise RetryLimitError + else: + break + return f(*args, **kwargs) + return wrapped + def post(self, vehicle_id, command, data={}): - return self.__connection.post('vehicles/%i/%s' % (vehicle_id, command), data) + return self.__connection.post('vehicles/%i/%s' % + (vehicle_id, command), data) def get(self, vehicle_id, command): return self.__connection.get('vehicles/%i/%s' % (vehicle_id, command)) - def data_request(self, vehicle_id, name): + @wake_up + def data_request(self, vehicle_id, name, wake_if_asleep=False): return self.get(vehicle_id, 'data_request/%s' % name)['response'] - def command(self, vehicle_id, name, data={}): + @wake_up + def command(self, vehicle_id, name, data={}, wake_if_asleep=True): return self.post(vehicle_id, 'command/%s' % name, data) def list_vehicles(self): return self.__vehicles - def wake_up(f): - @wraps(f) - def wrapped(inst, *args, **kwargs): - retries = 0 - sleep_delay = 2 - while True: - result = inst._wake_up(*args) - if not result: - if retries < 5: - time.sleep(sleep_delay) - retries += 1 - continue - else: - raise RetryLimitError - else: - break - return f(inst, *args, **kwargs) - return wrapped - - def _wake_up(self, vehicle_id): + def _wake_up(self, vehicle_id, *args, **kwargs): cur_time = int(time.time()) - if not self.__car_online[vehicle_id] or cur_time - self.__last_wake_up_time[vehicle_id] > 300: - self.__car_online[vehicle_id] = self.post(vehicle_id, 'wake_up')['response']['state'] == 'online' + if (not self.__car_online[vehicle_id] or + (cur_time - self.__last_wake_up_time[vehicle_id] > 300)): + result = self.post(vehicle_id, 'wake_up')['response']['state'] + _LOGGER.debug("Wakeup %s: %s" % (vehicle_id, result)) + self.__car_online[vehicle_id] = result == 'online' self.__last_wake_up_time[vehicle_id] = cur_time return self.__car_online[vehicle_id] @wake_up - def update(self, car_id): + def update(self, car_id, wake_if_asleep=False): cur_time = time.time() with self.__lock: if (self.__update[car_id] and - (cur_time - self.__last_update_time[car_id] > self.update_interval)): + ((cur_time - self.__last_update_time[car_id]) > + self.update_interval)): data = self.get(car_id, 'data') if data and data['response']: self.__climate[car_id] = data['response']['climate_state'] @@ -102,6 +150,8 @@ def update(self, car_id): self.__state[car_id] = data['response']['vehicle_state'] self.__driving[car_id] = data['response']['drive_state'] self.__gui[car_id] = data['response']['gui_settings'] + self.__car_online[car_id] = (data['response']['state'] + == 'online') self.__last_update_time[car_id] = time.time() else: self.__climate[car_id] = False @@ -109,34 +159,28 @@ def update(self, car_id): self.__state[car_id] = False self.__driving[car_id] = False self.__gui[car_id] = False + self.__car_online[car_id] = False - @wake_up def get_climate_params(self, car_id): return self.__climate[car_id] - @wake_up def get_charging_params(self, car_id): return self.__charging[car_id] - @wake_up def get_state_params(self, car_id): return self.__state[car_id] - @wake_up def get_drive_params(self, car_id): return self.__driving[car_id] - @wake_up def get_gui_params(self, car_id): return self.__gui[car_id] - @wake_up def get_updates(self, car_id=None): if car_id is not None: return self.__update[car_id] else: return self.__update - @wake_up def set_updates(self, car_id, value): self.__update[car_id] = value From fbf237185522177645eb23ac272519a2a2440c93 Mon Sep 17 00:00:00 2001 From: Alan Tse Date: Mon, 11 Feb 2019 21:01:17 -0800 Subject: [PATCH 04/13] Add logic to only poll awake vehicles (#19) * Disable updates for asleep vehicles * Change delay rate to exponential growth --- teslajsonpy/controller.py | 79 ++++++++++++++++++++++----------------- teslajsonpy/vehicle.py | 8 +++- 2 files changed, 52 insertions(+), 35 deletions(-) diff --git a/teslajsonpy/controller.py b/teslajsonpy/controller.py index b46ea80b..e3cb440e 100644 --- a/teslajsonpy/controller.py +++ b/teslajsonpy/controller.py @@ -25,18 +25,23 @@ def __init__(self, email, password, update_interval): self.__state = {} self.__driving = {} self.__gui = {} - self.__last_update_time = {} - self.__last_wake_up_time = {} + self._last_update_time = {} + self._last_wake_up_time = {} self.__lock = RLock() - self.__car_online = {} + self._car_online = {} - cars = self.__connection.get('vehicles')['response'] + cars = self.get_vehicles() for car in cars: - self.__last_update_time[car['id']] = 0 - self.__last_wake_up_time[car['id']] = 0 - self.__update[car['id']] = True - self.__car_online[car['id']] = False + self._last_update_time[car['id']] = 0 + self._last_wake_up_time[car['id']] = 0 + self._car_online[car['id']] = car['state'] + self.__climate[car['id']] = False + self.__charging[car['id']] = False + self.__state[car['id']] = False + self.__driving[car['id']] = False + self.__gui[car['id']] = False + try: self.update(car['id'], wake_if_asleep=False) except (TeslaException, RetryLimitError): @@ -75,8 +80,8 @@ def wrapped(*args, **kwargs): inst = args[0] vehicle_id = args[1] result = None - if (vehicle_id is not None and vehicle_id in inst.__car_online and - inst.__car_online[vehicle_id]): + if (vehicle_id is not None and vehicle_id in inst._car_online and + inst._car_online[vehicle_id]): try: result = f(*args, **kwargs) except (TeslaException): @@ -84,21 +89,21 @@ def wrapped(*args, **kwargs): if result is not None: return result else: - _LOGGER.debug("Wrapper: f:%s, result:%s, args:%s, kwargs:%s, \ - inst:%s, vehicle_id:%s, __cars_online:%s" % + _LOGGER.debug("Wrapper: f:%s, result:%s, args:%s, kwargs:%s, " + "inst:%s, vehicle_id:%s, _car_online:%s" % (f, result, args, kwargs, inst, vehicle_id, - inst.__car_online)) + inst._car_online)) while ('wake_if_asleep' in kwargs and kwargs['wake_if_asleep'] and (vehicle_id is None or (vehicle_id is not None and - vehicle_id in inst.__car_online and - not inst.__car_online[vehicle_id]))): + vehicle_id in inst._car_online and + not inst._car_online[vehicle_id]))): result = inst._wake_up(vehicle_id, *args, **kwargs) _LOGGER.debug("Result(%s): %s" % (retries, result)) if not result: if retries < 5: - time.sleep(sleep_delay) + time.sleep(sleep_delay**(retries+2)) retries += 1 continue else: @@ -108,6 +113,9 @@ def wrapped(*args, **kwargs): return f(*args, **kwargs) return wrapped + def get_vehicles(self): + return self.__connection.get('vehicles')['response'] + def post(self, vehicle_id, command, data={}): return self.__connection.post('vehicles/%i/%s' % (vehicle_id, command), data) @@ -128,38 +136,41 @@ def list_vehicles(self): def _wake_up(self, vehicle_id, *args, **kwargs): cur_time = int(time.time()) - if (not self.__car_online[vehicle_id] or - (cur_time - self.__last_wake_up_time[vehicle_id] > 300)): + if (not self._car_online[vehicle_id] or + (cur_time - self._last_wake_up_time[vehicle_id] > 300)): result = self.post(vehicle_id, 'wake_up')['response']['state'] _LOGGER.debug("Wakeup %s: %s" % (vehicle_id, result)) - self.__car_online[vehicle_id] = result == 'online' - self.__last_wake_up_time[vehicle_id] = cur_time - return self.__car_online[vehicle_id] + self._car_online[vehicle_id] = result == 'online' + self._last_wake_up_time[vehicle_id] = cur_time + return self._car_online[vehicle_id] @wake_up def update(self, car_id, wake_if_asleep=False): cur_time = time.time() with self.__lock: - if (self.__update[car_id] and - ((cur_time - self.__last_update_time[car_id]) > + # Check if any vehicles have been updated recently + last_update = max(self._last_update_time.values()) + if (cur_time - last_update > self.update_interval): + cars = self.get_vehicles() + for car in cars: + self._car_online[car['id']] = car['state'] + # Only update online vehicles + if (self._car_online[car_id] and + ((cur_time - self._last_update_time[car_id]) > self.update_interval)): - data = self.get(car_id, 'data') + # Only update cars with update flag on + data = (None if (car_id in self.__update and + not self.__update[car_id]) else + self.get(car_id, 'data')) if data and data['response']: self.__climate[car_id] = data['response']['climate_state'] self.__charging[car_id] = data['response']['charge_state'] self.__state[car_id] = data['response']['vehicle_state'] self.__driving[car_id] = data['response']['drive_state'] self.__gui[car_id] = data['response']['gui_settings'] - self.__car_online[car_id] = (data['response']['state'] - == 'online') - self.__last_update_time[car_id] = time.time() - else: - self.__climate[car_id] = False - self.__charging[car_id] = False - self.__state[car_id] = False - self.__driving[car_id] = False - self.__gui[car_id] = False - self.__car_online[car_id] = False + self._car_online[car_id] = (data['response']['state'] + == 'online') + self._last_update_time[car_id] = time.time() def get_climate_params(self, car_id): return self.__climate[car_id] diff --git a/teslajsonpy/vehicle.py b/teslajsonpy/vehicle.py index 07a7f8ea..c8a10e34 100644 --- a/teslajsonpy/vehicle.py +++ b/teslajsonpy/vehicle.py @@ -16,7 +16,13 @@ def _uniq_name(self): str(self._vin[3]).upper(), self._vin, self.type) def id(self): - return self._id + return self._id + + def assumed_state(self): + return (not self._controller._car_online[self.id()] and + (self._controller._last_update_time[self.id()] - + self._controller._last_wake_up_time[self.id()] > + self._controller.update_interval)) @staticmethod def is_armable(): From 528c2093bd457d931d0d595c29e7d9a34bb0dabc Mon Sep 17 00:00:00 2001 From: Alan Tse Date: Mon, 11 Feb 2019 23:23:46 -0800 Subject: [PATCH 05/13] Fix bug where update key not set (#20) --- teslajsonpy/controller.py | 1 + 1 file changed, 1 insertion(+) diff --git a/teslajsonpy/controller.py b/teslajsonpy/controller.py index e3cb440e..b0fdfdb0 100644 --- a/teslajsonpy/controller.py +++ b/teslajsonpy/controller.py @@ -35,6 +35,7 @@ def __init__(self, email, password, update_interval): for car in cars: self._last_update_time[car['id']] = 0 self._last_wake_up_time[car['id']] = 0 + self.__update[car['id']] = True self._car_online[car['id']] = car['state'] self.__climate[car['id']] = False self.__charging[car['id']] = False From a79a51ec2bc83245e9574655c112646dddc9cd22 Mon Sep 17 00:00:00 2001 From: Alan Tse Date: Mon, 11 Feb 2019 23:46:16 -0800 Subject: [PATCH 06/13] Fix bug where car['state'] stored without online test (#21) --- teslajsonpy/controller.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/teslajsonpy/controller.py b/teslajsonpy/controller.py index b0fdfdb0..50114720 100644 --- a/teslajsonpy/controller.py +++ b/teslajsonpy/controller.py @@ -36,7 +36,7 @@ def __init__(self, email, password, update_interval): self._last_update_time[car['id']] = 0 self._last_wake_up_time[car['id']] = 0 self.__update[car['id']] = True - self._car_online[car['id']] = car['state'] + self._car_online[car['id']] = (car['state'] == 'online') self.__climate[car['id']] = False self.__charging[car['id']] = False self.__state[car['id']] = False @@ -154,7 +154,7 @@ def update(self, car_id, wake_if_asleep=False): if (cur_time - last_update > self.update_interval): cars = self.get_vehicles() for car in cars: - self._car_online[car['id']] = car['state'] + self._car_online[car['id']] = (car['state'] == 'online') # Only update online vehicles if (self._car_online[car_id] and ((cur_time - self._last_update_time[car_id]) > From a5977c72b228cdd0fa7ee62925055aef4c74085c Mon Sep 17 00:00:00 2001 From: "Alan D. Tse" Date: Tue, 19 Feb 2019 20:27:55 -0800 Subject: [PATCH 07/13] Fix bug where result in wake_up not checked for False --- teslajsonpy/controller.py | 20 +++++++++++++++----- 1 file changed, 15 insertions(+), 5 deletions(-) diff --git a/teslajsonpy/controller.py b/teslajsonpy/controller.py index 50114720..3c4427fa 100644 --- a/teslajsonpy/controller.py +++ b/teslajsonpy/controller.py @@ -87,7 +87,11 @@ def wrapped(*args, **kwargs): result = f(*args, **kwargs) except (TeslaException): pass - if result is not None: + if (result is not None and + (result or + (('result' in result and not result['result']) or + ('reason' in result and result['reason'] != + 'could_not_wake_buses')))): return result else: _LOGGER.debug("Wrapper: f:%s, result:%s, args:%s, kwargs:%s, " @@ -96,10 +100,15 @@ def wrapped(*args, **kwargs): inst._car_online)) while ('wake_if_asleep' in kwargs and kwargs['wake_if_asleep'] and - (vehicle_id is None or - (vehicle_id is not None and - vehicle_id in inst._car_online and - not inst._car_online[vehicle_id]))): + # 'could_not_wake_buses' may occur despite online state + ((result is not None and type(result) == dict and + ('reason' in result and + result['reason'] != 'could_not_wake_buses')) or + # Check online state + (vehicle_id is None or + (vehicle_id is not None and + vehicle_id in inst._car_online and + not inst._car_online[vehicle_id])))): result = inst._wake_up(vehicle_id, *args, **kwargs) _LOGGER.debug("Result(%s): %s" % (retries, result)) if not result: @@ -108,6 +117,7 @@ def wrapped(*args, **kwargs): retries += 1 continue else: + inst._car_online[vehicle_id] = False raise RetryLimitError else: break From db1857134129f04026c91c1a9160862905c6fb32 Mon Sep 17 00:00:00 2001 From: Alan Tse Date: Fri, 22 Feb 2019 00:37:25 -0800 Subject: [PATCH 08/13] Fix throttling of get_vehicles (#27) * Add throttle for get_vehicles() call * Fix check on response for True boolean * Fix update to return boolean for wake_up wrapper * Add check for dict --- teslajsonpy/controller.py | 103 ++++++++++++++++++++++++++------------ 1 file changed, 72 insertions(+), 31 deletions(-) diff --git a/teslajsonpy/controller.py b/teslajsonpy/controller.py index 3c4427fa..3be3139b 100644 --- a/teslajsonpy/controller.py +++ b/teslajsonpy/controller.py @@ -25,12 +25,14 @@ def __init__(self, email, password, update_interval): self.__state = {} self.__driving = {} self.__gui = {} - self._last_update_time = {} - self._last_wake_up_time = {} + self._last_update_time = {} # succesful attempts by car + self._last_wake_up_time = {} # succesful wake_ups by car + self._last_attempted_update_time = 0 # all attempts by controller self.__lock = RLock() self._car_online = {} cars = self.get_vehicles() + self._last_attempted_update_time = time.time() for car in cars: self._last_update_time[car['id']] = 0 @@ -69,6 +71,8 @@ def wake_up(f): Args: inst (Controller): The instance of a controller vehicle_id (string): The vehicle to attempt to wake. + TODO: This currently requires a vehicle_id, but update() does not; This + should also be updated to allow that case wake_if_asleep (bool): Keyword arg to force a vehicle awake. Must be set in the wrapped function f Throws: @@ -87,16 +91,21 @@ def wrapped(*args, **kwargs): result = f(*args, **kwargs) except (TeslaException): pass + # Tesla API can return a dict with a bool in 'result', bool, or + # None. This clause will check for all conditions and also + # the reason isn't a failure to wake_buses. if (result is not None and - (result or - (('result' in result and not result['result']) or - ('reason' in result and result['reason'] != - 'could_not_wake_buses')))): + (result is True or + ((type(result) == dict and + ('result' in result and not result['result'] or + 'reason' in result and result['reason'] != + 'could_not_wake_buses'))))): return result else: - _LOGGER.debug("Wrapper: f:%s, result:%s, args:%s, kwargs:%s, " - "inst:%s, vehicle_id:%s, _car_online:%s" % - (f, result, args, kwargs, inst, vehicle_id, + _LOGGER.debug("Wrapped %s fails with %s \n" + "Additional info: args:%s, kwargs:%s, " + "vehicle_id:%s, _car_online:%s" % + (f, result, args, kwargs, vehicle_id, inst._car_online)) while ('wake_if_asleep' in kwargs and kwargs['wake_if_asleep'] and @@ -110,7 +119,7 @@ def wrapped(*args, **kwargs): vehicle_id in inst._car_online and not inst._car_online[vehicle_id])))): result = inst._wake_up(vehicle_id, *args, **kwargs) - _LOGGER.debug("Result(%s): %s" % (retries, result)) + _LOGGER.debug("Wake Attempt(%s): %s" % (retries, result)) if not result: if retries < 5: time.sleep(sleep_delay**(retries+2)) @@ -156,32 +165,64 @@ def _wake_up(self, vehicle_id, *args, **kwargs): return self._car_online[vehicle_id] @wake_up - def update(self, car_id, wake_if_asleep=False): + def update(self, car_id=None, wake_if_asleep=False, force=False): + """Updates all vehicle attributes in the cache. + + This command will connect to the Tesla API and first update the list of + online vehicles assuming no attempt for at least the [update_interval]. + It will then update all the cached values for cars that are awake + assuming no update has occurred for at least the [update_interval]. + + Args: + inst (Controller): The instance of a controller + car_id (string): The vehicle to update. If None, all cars are updated. + wake_if_asleep (bool): Keyword arg to force a vehicle awake. This is + processed by the wake_up decorator. + force (bool): Keyword arg to force a vehicle update regardless of the + update_interval + + Returns: + True if any update succeeded for any vehicle else false + Throws: + RetryLimitError + """ cur_time = time.time() with self.__lock: - # Check if any vehicles have been updated recently - last_update = max(self._last_update_time.values()) - if (cur_time - last_update > self.update_interval): + # Update the online cars using get_vehicles() + last_update = self._last_attempted_update_time + if (force or cur_time - last_update > self.update_interval): cars = self.get_vehicles() for car in cars: self._car_online[car['id']] = (car['state'] == 'online') - # Only update online vehicles - if (self._car_online[car_id] and - ((cur_time - self._last_update_time[car_id]) > - self.update_interval)): - # Only update cars with update flag on - data = (None if (car_id in self.__update and - not self.__update[car_id]) else - self.get(car_id, 'data')) - if data and data['response']: - self.__climate[car_id] = data['response']['climate_state'] - self.__charging[car_id] = data['response']['charge_state'] - self.__state[car_id] = data['response']['vehicle_state'] - self.__driving[car_id] = data['response']['drive_state'] - self.__gui[car_id] = data['response']['gui_settings'] - self._car_online[car_id] = (data['response']['state'] - == 'online') - self._last_update_time[car_id] = time.time() + self._last_attempted_update_time = cur_time + # Only update online vehicles that haven't been updated recently + # The throttling is per car's last succesful update + # Note: This separate check is because there may be individual cars + # to update. + update_succeeded = False + for id, v in self._car_online.items(): + # If specific car_id provided, only update match + if (car_id is not None and car_id != id): + continue + if (v and + (id in self.__update and self.__update[id]) and + (force or id not in self._last_update_time or + ((cur_time - self._last_update_time[id]) > + self.update_interval))): + # Only update cars with update flag on + data = self.get(id, 'data') + if data and data['response']: + response = data['response'] + self.__climate[car_id] = response['climate_state'] + self.__charging[car_id] = response['charge_state'] + self.__state[car_id] = response['vehicle_state'] + self.__driving[car_id] = response['drive_state'] + self.__gui[car_id] = response['gui_settings'] + self._car_online[car_id] = (response['state'] + == 'online') + self._last_update_time[car_id] = time.time() + update_succeeded = True + return update_succeeded def get_climate_params(self, car_id): return self.__climate[car_id] From 7dbe8fa4213756917286aae953205839a6d67ba5 Mon Sep 17 00:00:00 2001 From: Alan Tse Date: Sun, 3 Mar 2019 02:10:01 -0800 Subject: [PATCH 09/13] Clean up and address wake_up bug (#30) * Add throttle for get_vehicles() call * Fix check on response for True boolean * Fix update to return boolean for wake_up wrapper * Add check for dict * Add debugging to connection.py * Fix bug where check did not properly unwrap response --- teslajsonpy/connection.py | 5 ++ teslajsonpy/controller.py | 110 ++++++++++++++++++++++---------------- 2 files changed, 68 insertions(+), 47 deletions(-) diff --git a/teslajsonpy/connection.py b/teslajsonpy/connection.py index e857bec7..44d5f801 100644 --- a/teslajsonpy/connection.py +++ b/teslajsonpy/connection.py @@ -4,7 +4,9 @@ from urllib.request import Request, build_opener from urllib.error import HTTPError import json +import logging from teslajsonpy.Exceptions import TeslaException +_LOGGER = logging.getLogger(__name__) class Connection(object): @@ -51,6 +53,8 @@ def __open(self, url, headers={}, data=None, baseurl=""): if not baseurl: baseurl = self.baseurl req = Request("%s%s" % (baseurl, url), headers=headers) + _LOGGER.debug(url) + try: req.data = urlencode(data).encode('utf-8') except TypeError: @@ -62,6 +66,7 @@ def __open(self, url, headers={}, data=None, baseurl=""): charset = resp.info().get('charset', 'utf-8') data = json.loads(resp.read().decode(charset)) opener.close() + _LOGGER.debug(json.dumps(data)) return data except HTTPError as e: if e.code == 408: diff --git a/teslajsonpy/controller.py b/teslajsonpy/controller.py index 3be3139b..985f2e85 100644 --- a/teslajsonpy/controller.py +++ b/teslajsonpy/controller.py @@ -80,6 +80,33 @@ def wake_up(f): """ @wraps(f) def wrapped(*args, **kwargs): + def valid_result(result): + """Is TeslaAPI result succesful. + + Parameters + ---------- + result : tesla API result + This is the result of a Tesla Rest API call. + + Returns + ------- + bool + Tesla API failure can be checked in a dict with a bool in + ['response']['result'], a bool, or None or + ['response']['reason'] == 'could_not_wake_buses' + Returns true when a failure state not detected. + """ + return (result is not None and + (result is True or + (isinstance(result, dict) and + isinstance(result['response'], dict) and + ('result' in result['response'] and + result['response']['result'] is True) or + ('reason' in result['response'] and + result['response']['reason'] != + 'could_not_wake_buses') or + ('result' not in result['response'])))) + retries = 0 sleep_delay = 2 inst = args[0] @@ -89,67 +116,57 @@ def wrapped(*args, **kwargs): inst._car_online[vehicle_id]): try: result = f(*args, **kwargs) - except (TeslaException): + except TeslaException: pass - # Tesla API can return a dict with a bool in 'result', bool, or - # None. This clause will check for all conditions and also - # the reason isn't a failure to wake_buses. - if (result is not None and - (result is True or - ((type(result) == dict and - ('result' in result and not result['result'] or - 'reason' in result and result['reason'] != - 'could_not_wake_buses'))))): + if valid_result(result): return result - else: - _LOGGER.debug("Wrapped %s fails with %s \n" - "Additional info: args:%s, kwargs:%s, " - "vehicle_id:%s, _car_online:%s" % - (f, result, args, kwargs, vehicle_id, - inst._car_online)) - while ('wake_if_asleep' in kwargs and kwargs['wake_if_asleep'] - and - # 'could_not_wake_buses' may occur despite online state - ((result is not None and type(result) == dict and - ('reason' in result and - result['reason'] != 'could_not_wake_buses')) or - # Check online state - (vehicle_id is None or - (vehicle_id is not None and - vehicle_id in inst._car_online and - not inst._car_online[vehicle_id])))): - result = inst._wake_up(vehicle_id, *args, **kwargs) - _LOGGER.debug("Wake Attempt(%s): %s" % (retries, result)) - if not result: - if retries < 5: - time.sleep(sleep_delay**(retries+2)) - retries += 1 - continue - else: - inst._car_online[vehicle_id] = False - raise RetryLimitError + _LOGGER.debug("Wrapped %s -> %s \n" + "Additional info: args:%s, kwargs:%s, " + "vehicle_id:%s, _car_online:%s", + f.__name__, result, args, kwargs, vehicle_id, + inst._car_online) + inst._car_online[vehicle_id] = False + while ('wake_if_asleep' in kwargs and kwargs['wake_if_asleep'] + and not valid_result(result) and + # Check online state + (vehicle_id is None or + (vehicle_id is not None and + vehicle_id in inst._car_online and + not inst._car_online[vehicle_id]))): + result = inst._wake_up(vehicle_id, *args, **kwargs) + _LOGGER.debug("Wake Attempt(%s): %s" % (retries, result)) + if not valid_result(result): + if retries < 5: + time.sleep(sleep_delay**(retries+2)) + retries += 1 + continue else: - break - return f(*args, **kwargs) + inst._car_online[vehicle_id] = False + raise RetryLimitError + else: + break + return f(*args, **kwargs) return wrapped def get_vehicles(self): return self.__connection.get('vehicles')['response'] - def post(self, vehicle_id, command, data={}): + @wake_up + def post(self, vehicle_id, command, data={}, wake_if_asleep=True): return self.__connection.post('vehicles/%i/%s' % (vehicle_id, command), data) - def get(self, vehicle_id, command): + @wake_up + def get(self, vehicle_id, command, wake_if_asleep=False): return self.__connection.get('vehicles/%i/%s' % (vehicle_id, command)) - @wake_up def data_request(self, vehicle_id, name, wake_if_asleep=False): - return self.get(vehicle_id, 'data_request/%s' % name)['response'] + return self.get(vehicle_id, 'data_request/%s' % name, + wake_if_asleep=False)['response'] - @wake_up def command(self, vehicle_id, name, data={}, wake_if_asleep=True): - return self.post(vehicle_id, 'command/%s' % name, data) + return self.post(vehicle_id, 'command/%s' % name, data, + wake_if_asleep=True) def list_vehicles(self): return self.__vehicles @@ -164,7 +181,6 @@ def _wake_up(self, vehicle_id, *args, **kwargs): self._last_wake_up_time[vehicle_id] = cur_time return self._car_online[vehicle_id] - @wake_up def update(self, car_id=None, wake_if_asleep=False, force=False): """Updates all vehicle attributes in the cache. @@ -210,7 +226,7 @@ def update(self, car_id=None, wake_if_asleep=False, force=False): ((cur_time - self._last_update_time[id]) > self.update_interval))): # Only update cars with update flag on - data = self.get(id, 'data') + data = self.get(id, 'data', wake_if_asleep) if data and data['response']: response = data['response'] self.__climate[car_id] = response['climate_state'] From 2c45f2d5691d0dc2ccb51239b121484e29c8b736 Mon Sep 17 00:00:00 2001 From: Alan Tse Date: Sun, 3 Mar 2019 14:10:11 -0800 Subject: [PATCH 10/13] Fix potential infinite recursion (#31) * Fix potential infinite recursion on wake_up with wrapper --- teslajsonpy/controller.py | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/teslajsonpy/controller.py b/teslajsonpy/controller.py index 985f2e85..f84a404a 100644 --- a/teslajsonpy/controller.py +++ b/teslajsonpy/controller.py @@ -9,8 +9,8 @@ from teslajsonpy.GPS import GPS, Odometer from teslajsonpy.Exceptions import TeslaException from functools import wraps -from .Exceptions import RetryLimitError import logging +from .Exceptions import RetryLimitError _LOGGER = logging.getLogger(__name__) @@ -127,7 +127,7 @@ def valid_result(result): inst._car_online) inst._car_online[vehicle_id] = False while ('wake_if_asleep' in kwargs and kwargs['wake_if_asleep'] - and not valid_result(result) and + and # Check online state (vehicle_id is None or (vehicle_id is not None and @@ -135,7 +135,7 @@ def valid_result(result): not inst._car_online[vehicle_id]))): result = inst._wake_up(vehicle_id, *args, **kwargs) _LOGGER.debug("Wake Attempt(%s): %s" % (retries, result)) - if not valid_result(result): + if not result: if retries < 5: time.sleep(sleep_delay**(retries+2)) retries += 1 @@ -175,10 +175,14 @@ def _wake_up(self, vehicle_id, *args, **kwargs): cur_time = int(time.time()) if (not self._car_online[vehicle_id] or (cur_time - self._last_wake_up_time[vehicle_id] > 300)): - result = self.post(vehicle_id, 'wake_up')['response']['state'] - _LOGGER.debug("Wakeup %s: %s" % (vehicle_id, result)) - self._car_online[vehicle_id] = result == 'online' + result = self.post(vehicle_id, + 'wake_up', + wake_if_asleep=False) # avoid wrapper loop + self._car_online[vehicle_id] = (result['response']['state'] == + 'online') self._last_wake_up_time[vehicle_id] = cur_time + _LOGGER.debug("Wakeup %s: %s", vehicle_id, + result['response']['state']) return self._car_online[vehicle_id] def update(self, car_id=None, wake_if_asleep=False, force=False): From 9229b10dd23585252fc35e511dc54619d02cc397 Mon Sep 17 00:00:00 2001 From: Alan Tse Date: Sun, 3 Mar 2019 17:21:50 -0800 Subject: [PATCH 11/13] Add more debugging and retry for could_not_wake_buses error (#32) * Add further wrapper retries once car woken up to handle `could_not_wake_buses` error * Add connection timeout debugging * Fix pylint indentation errors --- teslajsonpy/connection.py | 1 + teslajsonpy/controller.py | 57 +++++++++++++++++++++++++-------------- 2 files changed, 38 insertions(+), 20 deletions(-) diff --git a/teslajsonpy/connection.py b/teslajsonpy/connection.py index 44d5f801..38923f61 100644 --- a/teslajsonpy/connection.py +++ b/teslajsonpy/connection.py @@ -70,6 +70,7 @@ def __open(self, url, headers={}, data=None, baseurl=""): return data except HTTPError as e: if e.code == 408: + _LOGGER.debug("Request timedout: %s", e) return False else: raise TeslaException(e.code) diff --git a/teslajsonpy/controller.py b/teslajsonpy/controller.py index f84a404a..ae715bb0 100644 --- a/teslajsonpy/controller.py +++ b/teslajsonpy/controller.py @@ -67,7 +67,8 @@ def wake_up(f): The command f is run once if the vehicle_id was last reported online. Assuming f returns None and wake_if_asleep is True, 5 attempts - will be made to wake the vehicle to reissue the command. + will be made to wake the vehicle to reissue the command. In addition, + if there is a `could_not_wake_buses` error, it will retry the command Args: inst (Controller): The instance of a controller vehicle_id (string): The vehicle to attempt to wake. @@ -96,17 +97,19 @@ def valid_result(result): ['response']['reason'] == 'could_not_wake_buses' Returns true when a failure state not detected. """ - return (result is not None and - (result is True or - (isinstance(result, dict) and - isinstance(result['response'], dict) and - ('result' in result['response'] and - result['response']['result'] is True) or - ('reason' in result['response'] and - result['response']['reason'] != - 'could_not_wake_buses') or - ('result' not in result['response'])))) - + try: + return (result is not None and result is not False and + (result is True or + (isinstance(result, dict) and + isinstance(result['response'], dict) and + ('result' in result['response'] and + result['response']['result'] is True) or + ('reason' in result['response'] and + result['response']['reason'] != + 'could_not_wake_buses') or + ('result' not in result['response'])))) + except TypeError as exception: + _LOGGER.error("Result: %s, %s", result, exception) retries = 0 sleep_delay = 2 inst = args[0] @@ -127,14 +130,14 @@ def valid_result(result): inst._car_online) inst._car_online[vehicle_id] = False while ('wake_if_asleep' in kwargs and kwargs['wake_if_asleep'] - and - # Check online state - (vehicle_id is None or - (vehicle_id is not None and - vehicle_id in inst._car_online and - not inst._car_online[vehicle_id]))): + and + # Check online state + (vehicle_id is None or + (vehicle_id is not None and + vehicle_id in inst._car_online and + not inst._car_online[vehicle_id]))): result = inst._wake_up(vehicle_id, *args, **kwargs) - _LOGGER.debug("Wake Attempt(%s): %s" % (retries, result)) + _LOGGER.debug("Wake Attempt(%s): %s", retries, result) if not result: if retries < 5: time.sleep(sleep_delay**(retries+2)) @@ -145,7 +148,21 @@ def valid_result(result): raise RetryLimitError else: break - return f(*args, **kwargs) + # try function five more times + retries = 0 + while True: + try: + result = f(*args, **kwargs) + _LOGGER.debug("Retry Attempt(%s): %s", retries, result) + except TeslaException: + pass + finally: + retries += 1 + time.sleep(sleep_delay**(retries+1)) + if valid_result(result): + return result + elif retries >= 5: + raise RetryLimitError return wrapped def get_vehicles(self): From 60fd1946171759b096f4e9407b3400f54048596d Mon Sep 17 00:00:00 2001 From: Alan Tse Date: Sun, 3 Mar 2019 19:45:41 -0800 Subject: [PATCH 12/13] Fix bug where RetryLimitErrors stops further updates (#33) * Add catch for RetryLimitErrors in updates * Remove redundant debug info --- teslajsonpy/connection.py | 2 +- teslajsonpy/controller.py | 5 ++++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/teslajsonpy/connection.py b/teslajsonpy/connection.py index 38923f61..e1b83508 100644 --- a/teslajsonpy/connection.py +++ b/teslajsonpy/connection.py @@ -70,7 +70,7 @@ def __open(self, url, headers={}, data=None, baseurl=""): return data except HTTPError as e: if e.code == 408: - _LOGGER.debug("Request timedout: %s", e) + _LOGGER.debug("%s", e) return False else: raise TeslaException(e.code) diff --git a/teslajsonpy/controller.py b/teslajsonpy/controller.py index ae715bb0..70a1c77a 100644 --- a/teslajsonpy/controller.py +++ b/teslajsonpy/controller.py @@ -247,7 +247,10 @@ def update(self, car_id=None, wake_if_asleep=False, force=False): ((cur_time - self._last_update_time[id]) > self.update_interval))): # Only update cars with update flag on - data = self.get(id, 'data', wake_if_asleep) + try: + data = self.get(id, 'data', wake_if_asleep) + except RetryLimitError: + pass if data and data['response']: response = data['response'] self.__climate[car_id] = response['climate_state'] From 23e57d67673bee67f8fe062cd39b24a2a5b61de3 Mon Sep 17 00:00:00 2001 From: Alan Tse Date: Mon, 4 Mar 2019 20:39:27 -0800 Subject: [PATCH 13/13] Fix bug where updates may stop on RetryLimitError (#34) * Remove redundant debug info * Fix controller to populate data with None on exception --- teslajsonpy/controller.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/teslajsonpy/controller.py b/teslajsonpy/controller.py index 70a1c77a..a9bc722f 100644 --- a/teslajsonpy/controller.py +++ b/teslajsonpy/controller.py @@ -249,8 +249,8 @@ def update(self, car_id=None, wake_if_asleep=False, force=False): # Only update cars with update flag on try: data = self.get(id, 'data', wake_if_asleep) - except RetryLimitError: - pass + except TeslaException: + data = None if data and data['response']: response = data['response'] self.__climate[car_id] = response['climate_state']