class Controller: def __init__(self, email, password, update_interval): self.__connection = Connection(email, password) self.__vehicles = [] self.update_interval = update_interval self.__climate = {} self.__charging = {} self.__state = {} self.__driving = {} self.__gui = {} self.__last_update_time = {} self.__lock = RLock() cars = self.__connection.get('vehicles')['response'] for car in cars: self.__last_update_time[car['id']] = 0 self.update(car['id']) self.__vehicles.append(Climate(car, self)) self.__vehicles.append(Battery(car, self)) self.__vehicles.append(Range(car, self)) self.__vehicles.append(TempSensor(car, self)) self.__vehicles.append(Lock(car, self)) self.__vehicles.append(ChargerConnectionSensor(car, self)) self.__vehicles.append(ChargerSwitch(car, self)) self.__vehicles.append(RangeSwitch(car, self)) self.__vehicles.append(ParkingSensor(car, self)) self.__vehicles.append(GPS(car, self)) self.__vehicles.append(Odometer(car, self)) def post(self, 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): return self.get(vehicle_id, 'data_request/%s' % name)['response'] def command(self, vehicle_id, name, data={}): return self.post(vehicle_id, 'command/%s' % name, data) def list_vehicles(self): return self.__vehicles def wake_up(self, vehicle_id): self.post(vehicle_id, 'wake_up') def update(self, car_id): cur_time = time.time() with self.__lock: if 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'] 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.__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 def get_climate_params(self, car_id): return self.__climate[car_id] def get_charging_params(self, car_id): return self.__charging[car_id] def get_state_params(self, car_id): return self.__state[car_id] def get_drive_params(self, car_id): return self.__driving[car_id] def get_gui_params(self, car_id): return self.__gui[car_id]
class Controller: """Controller for connections to Tesla Motors API.""" def __init__(self, email, password, update_interval): """Initialize controller. Parameters ---------- email : string Email of Tesla account password : type Password of Tesla account update_interval : type Seconds between allowed updates to the API. This is to prevent being blocked by Tesla Returns ------- None """ self.__connection = Connection(email, password) self.__vehicles = [] self.update_interval = update_interval self.__update = {} self.__climate = {} self.__charging = {} self.__state = {} self.__driving = {} self.__gui = {} 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 self._last_wake_up_time[car['id']] = 0 self.__update[car['id']] = True self.car_online[car['id']] = (car['state'] == 'online') 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): pass self.__vehicles.append(Climate(car, self)) self.__vehicles.append(Battery(car, self)) self.__vehicles.append(Range(car, self)) self.__vehicles.append(TempSensor(car, self)) self.__vehicles.append(Lock(car, self)) self.__vehicles.append(ChargerLock(car, self)) self.__vehicles.append(ChargerConnectionSensor(car, self)) self.__vehicles.append(ChargerSwitch(car, self)) self.__vehicles.append(RangeSwitch(car, self)) self.__vehicles.append(ParkingSensor(car, self)) self.__vehicles.append(GPS(car, self)) self.__vehicles.append(Odometer(car, self)) def wake_up(func): # pylint: disable=no-self-argument # issue is use of wraps on classmethods which should be replaced: # https://hynek.me/articles/decorators/ """Wrap 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. 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. 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: RetryLimitError """ @wraps(func) def wrapped(*args, **kwargs): # pylint: disable=too-many-branches,protected-access, not-callable def valid_result(result): """Check if 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. """ 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] 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 = func(*args, **kwargs) except TeslaException: pass if valid_result(result): return result _LOGGER.debug( "wake_up needed for %s -> %s \n" "Info: args:%s, kwargs:%s, " "vehicle_id:%s, car_online:%s", func.__name__, # pylint: disable=no-member 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 # 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) _LOGGER.debug( "%s(%s): Wake Attempt(%s): %s", func.__name__, # pylint: disable=no-member, vehicle_id, 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 else: break # try function five more times retries = 0 while True: try: result = func(*args, **kwargs) _LOGGER.debug( "%s(%s): Retry Attempt(%s): %s", func.__name__, # pylint: disable=no-member, vehicle_id, retries, result) except TeslaException: pass finally: retries += 1 time.sleep(sleep_delay**(retries + 1)) if valid_result(result): return result if retries >= 5: raise RetryLimitError return wrapped def get_vehicles(self): """Get vehicles json from TeslaAPI.""" return self.__connection.get('vehicles')['response'] @wake_up def post(self, vehicle_id, command, data=None, wake_if_asleep=True): # pylint: disable=unused-argument """Send post command to the vehicle_id. This is a wrapped function by wake_up. Parameters ---------- vehicle_id : string Identifier for the car on the owner-api endpoint. Confusingly it is not the vehicle_id field for identifying the car across different endpoints. https://tesla-api.timdorr.com/api-basics/vehicles#vehicle_id-vs-id command : string Tesla API command. https://tesla-api.timdorr.com/vehicle/commands data : dict Optional parameters. wake_if_asleep : bool Function for wake_up decorator indicating whether a failed response should wake up the vehicle or retry. Returns ------- dict Tesla json object. """ data = data or {} return self.__connection.post('vehicles/%i/%s' % (vehicle_id, command), data) @wake_up def get(self, vehicle_id, command, wake_if_asleep=False): # pylint: disable=unused-argument """Send get command to the vehicle_id. This is a wrapped function by wake_up. Parameters ---------- vehicle_id : string Identifier for the car on the owner-api endpoint. Confusingly it is not the vehicle_id field for identifying the car across different endpoints. https://tesla-api.timdorr.com/api-basics/vehicles#vehicle_id-vs-id command : string Tesla API command. https://tesla-api.timdorr.com/vehicle/commands wake_if_asleep : bool Function for wake_up decorator indicating whether a failed response should wake up the vehicle or retry. Returns ------- dict Tesla json object. """ return self.__connection.get('vehicles/%i/%s' % (vehicle_id, command)) def data_request(self, vehicle_id, name, wake_if_asleep=False): """Get requested data from vehicle_id. Parameters ---------- vehicle_id : string Identifier for the car on the owner-api endpoint. Confusingly it is not the vehicle_id field for identifying the car across different endpoints. https://tesla-api.timdorr.com/api-basics/vehicles#vehicle_id-vs-id name: string Name of data to be requested from the data_request endpoint which rolls ups all data plus vehicle configuration. https://tesla-api.timdorr.com/vehicle/state/data wake_if_asleep : bool Function for underlying api call for whether a failed response should wake up the vehicle or retry. Returns ------- dict Tesla json object. """ return self.get(vehicle_id, 'vehicle_data/%s' % name, wake_if_asleep=wake_if_asleep)['response'] def command(self, vehicle_id, name, data=None, wake_if_asleep=True): """Post name command to the vehicle_id. Parameters ---------- vehicle_id : string Identifier for the car on the owner-api endpoint. Confusingly it is not the vehicle_id field for identifying the car across different endpoints. https://tesla-api.timdorr.com/api-basics/vehicles#vehicle_id-vs-id name : string Tesla API command. https://tesla-api.timdorr.com/vehicle/commands data : dict Optional parameters. wake_if_asleep : bool Function for underlying api call for whether a failed response should wake up the vehicle or retry. Returns ------- dict Tesla json object. """ data = data or {} return self.post(vehicle_id, 'command/%s' % name, data, wake_if_asleep=wake_if_asleep) def list_vehicles(self): """Return list of Tesla components for Home Assistant setup. Use get_vehicles() for general API use. """ return self.__vehicles 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)): 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): """Update 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: # 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') 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_, value 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 (value and # pylint: disable=too-many-boolean-expressions (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 try: data = self.get(id_, 'data', wake_if_asleep) except TeslaException: data = None 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 cached copy of climate_params for car_id.""" return self.__climate[car_id] def get_charging_params(self, car_id): """Return cached copy of charging_params for car_id.""" return self.__charging[car_id] def get_state_params(self, car_id): """Return cached copy of state_params for car_id.""" return self.__state[car_id] def get_drive_params(self, car_id): """Return cached copy of drive_params for car_id.""" return self.__driving[car_id] def get_gui_params(self, car_id): """Return cached copy of gui_params for car_id.""" return self.__gui[car_id] def get_updates(self, car_id=None): """Get updates dictionary. Parameters ---------- car_id : string Identifier for the car on the owner-api endpoint. Confusingly it is not the vehicle_id field for identifying the car across different endpoints. https://tesla-api.timdorr.com/api-basics/vehicles#vehicle_id-vs-id If no car_id, returns the complete dictionary. Returns ------- bool or dict of booleans If car_id exists, a bool indicating whether updates should be procssed. Othewise, the entire updates dictionary. """ if car_id is not None: return self.__update[car_id] return self.__update def set_updates(self, car_id, value): """Set updates dictionary. Parameters ---------- car_id : string Identifier for the car on the owner-api endpoint. Confusingly it is not the vehicle_id field for identifying the car across different endpoints. https://tesla-api.timdorr.com/api-basics/vehicles#vehicle_id-vs-id value : bool Whether the specific car_id should be updated. Returns ------- None """ self.__update[car_id] = value