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 __init__( self, websession: Optional[httpx.AsyncClient] = None, email: Text = None, password: Text = None, access_token: Text = None, refresh_token: Text = None, expiration: int = 0, update_interval: int = 300, enable_websocket: bool = False, ) -> None: """Initialize controller. Args: websession (aiohttp.ClientSession): Websession for aiohttp. email (Text, optional): Email account. Defaults to None. password (Text, optional): Password. Defaults to None. access_token (Text, optional): Access token. Defaults to None. refresh_token (Text, optional): Refresh token. Defaults to None. expiration (int, optional): Timestamp when access_token expires. Defaults to 0 update_interval (int, optional): Seconds between allowed updates to the API. This is to prevent being blocked by Tesla. Defaults to 300. enable_websocket (bool, optional): Whether to connect with websockets. Defaults to False. """ self.__connection = Connection( websession=websession if websession and isinstance(websession, httpx.AsyncClient) else httpx.AsyncClient(timeout=60), email=email, password=password, access_token=access_token, refresh_token=refresh_token, expiration=expiration, ) self.__components = [] self._update_interval: int = update_interval self.__update = {} self.__climate = {} self.__charging = {} self.__state = {} self.__config = {} self.__driving = {} self.__gui = {} self._last_update_time = {} # succesful update 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 = {} self.__update_lock = None # controls access to update function self.__wakeup_conds = {} self.car_online = {} self.car_state = {} self.__id_vin_map = {} self.__vin_id_map = {} self.__vin_vehicle_id_map = {} self.__vehicle_id_vin_map = {} self.__websocket_listeners = [] self.__last_parked_timestamp = {} self.__update_state = {} self.enable_websocket = enable_websocket
class Controller: # pylint: disable=too-many-public-methods """Controller for connections to Tesla Motors API.""" def __init__( self, websession, email: Text = None, password: Text = None, access_token: Text = None, refresh_token: Text = None, expiration: int = 0, update_interval: int = 300, enable_websocket: bool = False, ) -> None: """Initialize controller. Args: websession (aiohttp.ClientSession): Websession for aiohttp. email (Text, optional): Email account. Defaults to None. password (Text, optional): Password. Defaults to None. access_token (Text, optional): Access token. Defaults to None. refresh_token (Text, optional): Refresh token. Defaults to None. expiration (int, optional): Timestamp when access_token expires. Defaults to 0 update_interval (int, optional): Seconds between allowed updates to the API. This is to prevent being blocked by Tesla. Defaults to 300. enable_websocket (bool, optional): Whether to connect with websockets. Defaults to False. """ self.__connection = Connection( websession=websession, email=email, password=password, access_token=access_token, refresh_token=refresh_token, expiration=expiration, ) self.__components = [] self._update_interval: int = update_interval self.__update = {} self.__climate = {} self.__charging = {} self.__state = {} self.__config = {} self.__driving = {} self.__gui = {} self._last_update_time = {} # succesful update 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 = {} self.__update_lock = None # controls access to update function self.__wakeup_conds = {} self.car_online = {} self.car_state = {} self.__id_vin_map = {} self.__vin_id_map = {} self.__vin_vehicle_id_map = {} self.__vehicle_id_vin_map = {} self.__websocket_listeners = [] self.__last_parked_timestamp = {} self.__update_state = {} self.enable_websocket = enable_websocket async def connect( self, test_login: bool = False, wake_if_asleep: bool = False, filtered_vins: Optional[List[Text]] = None, mfa_code: Text = "", ) -> Dict[Text, Text]: """Connect controller to Tesla. Args test_login (bool, optional): Whether to test credentials only. Defaults to False. wake_if_asleep (bool, optional): Whether to wake up any sleeping cars to update state. Defaults to False. filtered_vins (list, optional): If not empty, filters the cars by the provided VINs. mfa_code (Text, optional): MFA code to use for connection Returns Dict[Text, Text]: Returns the refresh_token, access_token, and expires_in time """ if mfa_code: self.__connection.mfa_code = mfa_code cars = await self.get_vehicles() self._last_attempted_update_time = time.time() self.__update_lock = asyncio.Lock() for car in cars: vin = car["vin"] if filtered_vins and vin not in filtered_vins: _LOGGER.debug("Skipping car with VIN: %s", vin) continue self.__id_vin_map[car["id"]] = vin self.__vin_id_map[vin] = car["id"] self.__vin_vehicle_id_map[vin] = car["vehicle_id"] self.__vehicle_id_vin_map[car["vehicle_id"]] = vin self.__lock[vin] = asyncio.Lock() self.__wakeup_conds[vin] = asyncio.Lock() self._last_update_time[vin] = 0 self._last_wake_up_time[vin] = 0 self.__update[vin] = True self.__update_state[vin] = "normal" self.car_state[vin] = car self.car_online[vin] = car["state"] == "online" self.__last_parked_timestamp[ vin] = self._last_attempted_update_time self.__climate[vin] = {} self.__charging[vin] = {} self.__state[vin] = {} self.__config[vin] = {} self.__driving[vin] = {} self.__gui[vin] = {} self._add_components(car) if not test_login: tasks = [ self.update(car["id"], wake_if_asleep=wake_if_asleep) for car in cars ] _LOGGER.debug("tasks %s %s", tasks, wake_if_asleep) try: await asyncio.gather(*tasks) except (TeslaException, RetryLimitError): pass return { "refresh_token": self.__connection.refresh_token, "access_token": self.__connection.access_token, "expiration": self.__connection.expiration, } def is_token_refreshed(self) -> bool: """Return whether token has been changed and not retrieved. Returns bool: Whether token has been changed since the last return """ return self.__connection.token_refreshed def get_tokens(self) -> Dict[Text, Text]: """Return oauth data including refresh and access tokens, and expires time. This will set the the self.__connection token_refreshed to False. Returns Dict[Text, Text]: Returns the refresh_token, access_token, and expires time """ self.__connection.token_refreshed = False return { "refresh_token": self.__connection.refresh_token, "access_token": self.__connection.access_token, "expiration": self.__connection.expiration, } def get_expiration(self) -> int: """Return expiration for oauth. Returns int: Returns timestamp when oauth expires """ return self.__connection.expiration def get_oauth_url(self) -> URL: """Return oauth url.""" return self.__connection.get_authorization_code_link(new=True) def set_authorization_code(self, code: Text) -> None: """Set authorization code in Connection.""" self.__connection.code = code def set_authorization_domain(self, domain: Text) -> None: """Set authorization domain in Connection.""" if not domain: return if self.__connection.auth_domain.host != domain: self.__connection.auth_domain = self.__connection.auth_domain.with_host( domain) def register_websocket_callback(self, callback) -> int: """Register callback for websocket messages. Args callback (function): function to call with json data Returns int: Return index of entry """ self.__websocket_listeners.append(callback) return len(self.__websocket_listeners) - 1 @backoff.on_exception(min_expo, ClientConnectorError, max_time=10, logger=__name__) async def get_vehicles(self): """Get vehicles json from TeslaAPI.""" return (await self.__connection.get("vehicles"))["response"] @wake_up async def post(self, car_id, command, data=None, wake_if_asleep=True): # pylint: disable=unused-argument """Send post command to the car_id. This is a wrapped function by wake_up. Parameters ---------- car_id : string Identifier for the car on the owner-api endpoint. It is the id field for identifying the car across the owner-api endpoint. 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. """ car_id = self._update_id(car_id) data = data or {} return await self.__connection.post(f"vehicles/{car_id}/{command}", data=data) @wake_up async def get(self, car_id, command, wake_if_asleep=False): # pylint: disable=unused-argument """Send get command to the car_id. This is a wrapped function by wake_up. Parameters ---------- car_id : string Identifier for the car on the owner-api endpoint. It is the id field for identifying the car across the owner-api endpoint. 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. """ car_id = self._update_id(car_id) return await self.__connection.get(f"vehicles/{car_id}/{command}") async def data_request(self, car_id, name, wake_if_asleep=False): """Get requested data from car_id. Parameters ---------- car_id : string Identifier for the car on the owner-api endpoint. It is the id field for identifying the car across the owner-api endpoint. 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. """ car_id = self._update_id(car_id) return (await self.get(car_id, f"vehicle_data/{name}", wake_if_asleep=wake_if_asleep))["response"] @backoff.on_exception(min_expo, TeslaException, max_time=60, logger=__name__, min_value=15) async def command(self, car_id, name, data=None, wake_if_asleep=True): """Post name command to the car_id. Parameters ---------- car_id : string Identifier for the car on the owner-api endpoint. It is the id field for identifying the car across the owner-api endpoint. 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. """ car_id = self._update_id(car_id) data = data or {} return await self.post(car_id, f"command/{name}", data=data, wake_if_asleep=wake_if_asleep) def get_homeassistant_components(self): """Return list of Tesla components for Home Assistant setup. Use get_vehicles() for general API use. """ return self.__components def _add_components(self, car): self.__components.append(Climate(car, self)) self.__components.append(Battery(car, self)) self.__components.append(Range(car, self)) self.__components.append(TempSensor(car, self)) self.__components.append(Lock(car, self)) self.__components.append(ChargerLock(car, self)) self.__components.append(ChargerConnectionSensor(car, self)) self.__components.append(ChargingSensor(car, self)) self.__components.append(ChargerSwitch(car, self)) self.__components.append(RangeSwitch(car, self)) self.__components.append(ParkingSensor(car, self)) self.__components.append(GPS(car, self)) self.__components.append(Odometer(car, self)) self.__components.append(OnlineSensor(car, self)) self.__components.append(SentryModeSwitch(car, self)) self.__components.append(TrunkLock(car, self)) self.__components.append(FrunkLock(car, self)) self.__components.append(UpdateSensor(car, self)) async def _wake_up(self, car_id): car_vin = self._id_to_vin(car_id) car_id = self._update_id(car_id) async with self.__wakeup_conds[car_vin]: cur_time = int(time.time()) if not self.car_online[car_vin] or ( cur_time - self._last_wake_up_time[car_vin] > self.update_interval): result = await self.post(car_id, "wake_up", wake_if_asleep=False ) # avoid wrapper loop self.car_online[car_vin] = result["response"][ "state"] == "online" self.car_state[car_vin] = result["response"] self._last_wake_up_time[car_vin] = cur_time _LOGGER.debug("Wakeup %s: %s", car_vin[-5:], self.car_state[car_vin]["state"]) return self.car_online[car_vin] async def update( self, car_id: Optional[Text] = None, wake_if_asleep: bool = False, force: bool = False, ) -> bool: # pylint: disable=too-many-locals,too-many-statements """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 car_id (Text, optional): The vehicle to update. If None, all cars are updated. Defaults to None. wake_if_asleep (bool, optional): force a vehicle awake. This is processed by the wake_up decorator. Defaults to False. force (bool, optional): force a vehicle update regardless of the update_interval. Defaults to False. Returns Whether update was successful. Raises RetryLimitError """ def _calculate_next_interval(vin: Text) -> int: cur_time = time.time() # _LOGGER.debug( # "%s: %s > %s; shift_state: %s sentry: %s climate: %s, charging: %s ", # vin[-5:], # cur_time - self.__last_parked_timestamp[vin], # IDLE_INTERVAL, # self.__driving[vin].get("shift_state"), # self.__state[vin].get("sentry_mode"), # self.__climate[vin].get("is_climate_on"), # self.__charging[vin].get("charging_state") == "Charging", # ) if self.car_state[vin].get("state") == "asleep" or self.__driving[ vin].get("shift_state"): _LOGGER.debug( "%s resetting last_parked_timestamp: shift_state %s", vin[-5:], self.__driving[vin].get("shift_state"), ) self.__last_parked_timestamp[vin] = cur_time if self.__driving[vin].get("shift_state") in ["D", "R"]: if self.__update_state[vin] != "driving": self.__update_state[vin] = "driving" _LOGGER.debug( "%s driving; increasing scan rate to every %s seconds", vin[-5:], DRIVING_INTERVAL, ) return DRIVING_INTERVAL if (cur_time - self.__last_parked_timestamp[vin] > IDLE_INTERVAL ) and not (self.__state[vin].get("sentry_mode") or self.__climate[vin].get("is_climate_on") or self.__charging[vin].get("charging_state") == "Charging"): sleep_interval = max(SLEEP_INTERVAL, self.update_interval) if self.__update_state[vin] != "trying_to_sleep": self.__update_state[vin] = "trying_to_sleep" _LOGGER.debug( "%s trying to sleep; scan throttled to %s seconds and will ignore updates for %s seconds", vin[-5:], sleep_interval, round( sleep_interval + self._last_update_time[vin] - cur_time, 2, ), ) return sleep_interval if self.__update_state[vin] != "normal": self.__update_state[vin] = "normal" _LOGGER.debug("%s scanning every %s seconds", vin[-5:], self.update_interval) return self.update_interval async def _get_and_process_data(vin: Text) -> None: async with self.__lock[vin]: _LOGGER.debug("Updating %s", vin[-5:]) try: data = await self.get( self.__vin_id_map[vin], "vehicle_data", wake_if_asleep=wake_if_asleep, ) except TeslaException: data = None if data and data["response"]: response = data["response"] self.__climate[vin] = response["climate_state"] self.__charging[vin] = response["charge_state"] self.__state[vin] = response["vehicle_state"] self.__config[vin] = response["vehicle_config"] if (self.__driving[vin].get("shift_state") and self.__driving[vin].get("shift_state") != response["drive_state"]["shift_state"] and (response["drive_state"]["shift_state"] is None or response["drive_state"]["shift_state"] == "P")): self.__last_parked_timestamp[vin] = ( response["drive_state"]["timestamp"] / 1000) self.__driving[vin] = response["drive_state"] self.__gui[vin] = response["gui_settings"] self._last_update_time[vin] = time.time() if (self.enable_websocket and self.get_drive_params( self.__vin_id_map[vin]).get("shift_state") and self.get_drive_params( self.__vin_id_map[vin]).get("shift_state") != "P"): asyncio.create_task( self.__connection.websocket_connect( vin[-5:], self.__vin_vehicle_id_map[vin], on_message=self._process_websocket_message, on_disconnect=self. _process_websocket_disconnect, )) async with self.__update_lock: cur_time = time.time() # Update the online cars using get_vehicles() last_update = self._last_attempted_update_time if force or cur_time - last_update > ONLINE_INTERVAL: cars = await self.get_vehicles() self.car_online = {} for car in cars: self.__id_vin_map[car["id"]] = car["vin"] self.__vin_id_map[car["vin"]] = car["id"] self.__vin_vehicle_id_map[car["vin"]] = car["vehicle_id"] self.__vehicle_id_vin_map[car["vehicle_id"]] = car["vin"] self.car_online[car["vin"]] = car["state"] == "online" self.car_state[car["vin"]] = car 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. car_id = self._update_id(car_id) car_vin = self._id_to_vin(car_id) tasks = [] for vin, online in self.car_online.items(): # If specific car_id provided, only update match if (car_vin and car_vin != vin ) or self.car_state[vin].get("in_service"): continue async with self.__lock[vin]: car_state = self.car_state[vin].get("state") if ((online or (wake_if_asleep and car_state in ["asleep", "offline"])) and ( # pylint: disable=too-many-boolean-expressions self.__update.get(vin)) and (force or vin not in self._last_update_time or ((cur_time - self._last_update_time[vin]) > _calculate_next_interval(vin))) ): # Only update cars with update flag on tasks.append(_get_and_process_data(vin)) else: _LOGGER.debug("Skipping update of %s with state %s", vin[-5:], car_state) return any(await asyncio.gather(*tasks)) def get_climate_params(self, car_id): """Return cached copy of climate_params for car_id.""" vin = self._id_to_vin(car_id) if vin: return self.__climate[vin] return {} def get_charging_params(self, car_id): """Return cached copy of charging_params for car_id.""" vin = self._id_to_vin(car_id) if vin: return self.__charging[vin] return {} def get_state_params(self, car_id): """Return cached copy of state_params for car_id.""" vin = self._id_to_vin(car_id) if vin: return self.__state[vin] return {} def get_config_params(self, car_id): """Return cached copy of state_params for car_id.""" vin = self._id_to_vin(car_id) if vin: return self.__config[vin] return {} def get_drive_params(self, car_id): """Return cached copy of drive_params for car_id.""" vin = self._id_to_vin(car_id) if vin: return self.__driving[vin] return {} def get_gui_params(self, car_id): """Return cached copy of gui_params for car_id.""" vin = self._id_to_vin(car_id) if vin: return self.__gui[vin] return {} def get_updates(self, car_id: Text = None): """Get updates dictionary. Parameters ---------- car_id : string Identifier for the car on the owner-api endpoint. It is the id field for identifying the car across the owner-api endpoint. 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 processed. Othewise, the entire updates dictionary. """ vin = self._id_to_vin(car_id) if vin: return self.__update[vin] return self.__update def set_updates(self, car_id: Text, value: bool) -> None: """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 """ vin = self._id_to_vin(car_id) if vin: self.__update[vin] = value def get_last_update_time(self, car_id: Text = None): """Get last_update time dictionary. Parameters ---------- car_id : string Identifier for the car on the owner-api endpoint. It is the id field for identifying the car across the owner-api endpoint. https://tesla-api.timdorr.com/api-basics/vehicles#vehicle_id-vs-id If no car_id, returns the complete dictionary. Returns ------- int or dict of ints If car_id exists, a int (time.time()) indicating when updates last processed. Othewise, the entire updates dictionary. """ vin = self._id_to_vin(car_id) if vin: return self._last_update_time[vin] return self._last_update_time @property def update_interval(self) -> int: """Return update_interval. Returns int: The number of seconds between updates """ return self._update_interval @update_interval.setter def update_interval(self, value: int) -> None: if value: self._update_interval = int(value) def _id_to_vin(self, car_id: Text) -> Optional[Text]: return self.__id_vin_map.get(car_id) def _update_id(self, car_id: Text) -> Optional[Text]: new_car_id = self.__vin_id_map.get(self._id_to_vin(car_id)) if new_car_id: car_id = new_car_id return car_id def _process_websocket_message(self, data): if data["msg_type"] == "data:update": update_json = {} vehicle_id = int(data["tag"]) vin = self.__vehicle_id_vin_map[vehicle_id] # shift_state,speed,power,est_lat,est_lng,est_heading,est_corrected_lat,est_corrected_lng, # native_latitude,native_longitude,native_heading,native_type,native_location_supported keys = [ ("timestamp", int), ("shift_state", str), ("speed", int), ("power", int), ("est_lat", float), ("est_lng", float), ("est_heading", int), ("est_corrected_lat", float), ("est_corrected_lng", float), ("native_latitude", float), ("native_longitude", float), ("native_heading", float), ("native_type", str), ("native_location_supported", int), # ("soc", int), # ("elevation", int), # ("range", int), # ("est_range", int), # ("heading", int), ] values = data["value"].split(",") try: for num, value in enumerate(values): update_json[keys[num][0]] = keys[num][1]( value) if value else None _LOGGER.debug("Updating %s with websocket: %s", vin[-5:], update_json) self.__driving[vin]["timestamp"] = update_json["timestamp"] if (self.__driving[vin].get("shift_state") and self.__driving[vin].get("shift_state") != update_json["shift_state"] and (update_json["shift_state"] is None or update_json["shift_state"] == "P")): self.__last_parked_timestamp[ vin] = update_json["timestamp"] / 1000 self.__driving[vin]["shift_state"] = update_json["shift_state"] self.__driving[vin]["speed"] = update_json["speed"] self.__driving[vin]["power"] = update_json["power"] self.__driving[vin]["latitude"] = update_json[ "est_corrected_lat"] self.__driving[vin]["longitude"] = update_json[ "est_corrected_lng"] self.__driving[vin]["heading"] = update_json["est_heading"] self.__driving[vin]["native_latitude"] = update_json[ "native_latitude"] self.__driving[vin]["native_longitude"] = update_json[ "native_longitude"] self.__driving[vin]["native_heading"] = update_json[ "native_heading"] self.__driving[vin]["native_type"] = update_json["native_type"] self.__driving[vin]["native_location_supported"] = update_json[ "native_location_supported"] # old values # self.__charging[vin]["timestamp"] = update_json["timestamp"] # self.__state[vin]["timestamp"] = update_json["timestamp"] # self.__state[vin]["odometer"] = update_json["odometer"] # self.__charging[vin]["battery_level"] = update_json["soc"] # self.__state[vin]["odometer"] = update_json["elevation"] # no current elevation stored # self.__charging[vin]["battery_range"] = update_json["range"] # self.__charging[vin]["est_battery_range"] = update_json["est_range"] # self.__driving[vin]["heading"] = update_json["heading"] # est_heading appears more accurate except ValueError as ex: _LOGGER.debug("Websocket for %s malformed: %s\n%s", vin[-5:], values, ex) for func in self.__websocket_listeners: func(data) def _process_websocket_disconnect(self, data): vehicle_id = int(data["tag"]) vin = self.__vehicle_id_vin_map[vehicle_id] _LOGGER.debug("Disconnected %s from websocket", vin[-5:])
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]
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))
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