From d463420f9c2ff4c305841f4bcfb5d9b570607858 Mon Sep 17 00:00:00 2001 From: "Alan D. Tse" Date: Sun, 26 Jan 2020 16:46:37 -0800 Subject: [PATCH 01/40] refactor: simplify kwargs logic --- teslajsonpy/controller.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/teslajsonpy/controller.py b/teslajsonpy/controller.py index 30db4c21..c0e1f95b 100644 --- a/teslajsonpy/controller.py +++ b/teslajsonpy/controller.py @@ -234,8 +234,7 @@ def valid_result(result): ) inst.car_online[inst._id_to_vin(car_id)] = False while ( - "wake_if_asleep" in kwargs - and kwargs["wake_if_asleep"] + kwargs.get("wake_if_asleep") and # Check online state ( From 5cf5c178704527c5fd4fcc65f77b3de8c60bbc53 Mon Sep 17 00:00:00 2001 From: "Alan D. Tse" Date: Sun, 26 Jan 2020 16:47:55 -0800 Subject: [PATCH 02/40] fix: save raw_online_state on updates --- teslajsonpy/controller.py | 1 + 1 file changed, 1 insertion(+) diff --git a/teslajsonpy/controller.py b/teslajsonpy/controller.py index c0e1f95b..c36e17b0 100644 --- a/teslajsonpy/controller.py +++ b/teslajsonpy/controller.py @@ -475,6 +475,7 @@ async def update(self, car_id=None, wake_if_asleep=False, force=False): self.__id_vin_map[car["id"]] = car["vin"] self.__vin_id_map[car["vin"]] = car["id"] self.car_online[car["vin"]] = car["state"] == "online" + self.raw_online_state[car["vin"]] = car["state"] 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 From 2c85b804b7a7f8d73dffc603cadaa00dc8315089 Mon Sep 17 00:00:00 2001 From: "Alan D. Tse" Date: Sun, 26 Jan 2020 16:48:35 -0800 Subject: [PATCH 03/40] refactor: only allow updates if car isn't offline --- teslajsonpy/controller.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/teslajsonpy/controller.py b/teslajsonpy/controller.py index c36e17b0..46964d3b 100644 --- a/teslajsonpy/controller.py +++ b/teslajsonpy/controller.py @@ -489,8 +489,9 @@ async def update(self, car_id=None, wake_if_asleep=False, force=False): if car_vin and car_vin != vin: continue async with self.__lock[vin]: + car_state = self.raw_online_state[vin] if ( - (online or wake_if_asleep) + (online or (wake_if_asleep and car_state == "asleep")) and ( # pylint: disable=too-many-boolean-expressions self.__update.get(vin) ) From fad88bc11337c6517a99db28154a668b2e506caa Mon Sep 17 00:00:00 2001 From: "Alan D. Tse" Date: Sun, 26 Jan 2020 16:49:16 -0800 Subject: [PATCH 04/40] fix: increase minimum retry delay to 15 seconds --- teslajsonpy/controller.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/teslajsonpy/controller.py b/teslajsonpy/controller.py index 46964d3b..3afb2a7f 100644 --- a/teslajsonpy/controller.py +++ b/teslajsonpy/controller.py @@ -256,7 +256,7 @@ def valid_result(result): ) if not result: if retries < 5: - await asyncio.sleep(sleep_delay ** (retries + 2)) + await asyncio.sleep(15 + sleep_delay ** (retries + 2)) retries += 1 continue inst.car_online[inst._id_to_vin(car_id)] = False @@ -272,7 +272,7 @@ def valid_result(result): kwargs, ) while not valid_result(result): - await asyncio.sleep(sleep_delay ** (retries + 1)) + await asyncio.sleep(15 + sleep_delay ** (retries + 1)) try: result = await func(*args, **kwargs) _LOGGER.debug( From 8483a2078b4cb3c8263447eba3c7b06a20793823 Mon Sep 17 00:00:00 2001 From: "Alan D. Tse" Date: Sun, 26 Jan 2020 16:53:24 -0800 Subject: [PATCH 05/40] fix: export OnlineSensor --- teslajsonpy/__init__.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/teslajsonpy/__init__.py b/teslajsonpy/__init__.py index 77db58d4..8383cbba 100644 --- a/teslajsonpy/__init__.py +++ b/teslajsonpy/__init__.py @@ -6,7 +6,11 @@ https://github.com/zabuldon/teslajsonpy """ from teslajsonpy.battery_sensor import Battery, Range -from teslajsonpy.binary_sensor import ChargerConnectionSensor, ParkingSensor +from teslajsonpy.binary_sensor import ( + ChargerConnectionSensor, + OnlineSensor, + ParkingSensor, +) from teslajsonpy.charger import ChargerSwitch, ChargingSensor, RangeSwitch from teslajsonpy.climate import Climate, TempSensor from teslajsonpy.controller import Controller @@ -21,6 +25,7 @@ "Range", "ChargerConnectionSensor", "ChargingSensor", + "OnlineSensor", "ParkingSensor", "ChargerSwitch", "RangeSwitch", From 7719abb5103fd862707eeab7fada783f8993f07c Mon Sep 17 00:00:00 2001 From: "Alan D. Tse" Date: Tue, 28 Jan 2020 00:37:09 -0800 Subject: [PATCH 06/40] feat: add websocket connection Closes #25 --- teslajsonpy/connection.py | 51 ++++++++++++++++++++++++++++++++++ teslajsonpy/controller.py | 58 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 109 insertions(+) diff --git a/teslajsonpy/connection.py b/teslajsonpy/connection.py index f14f30f5..ff7ff771 100644 --- a/teslajsonpy/connection.py +++ b/teslajsonpy/connection.py @@ -5,6 +5,7 @@ For more details about this api, please refer to the documentation at https://github.com/zabuldon/teslajsonpy """ +import asyncio import calendar import datetime import json @@ -39,6 +40,7 @@ def __init__( "c7257eb71a564034f9419ee651c7d0e5f7" "aa6bfbd18bafb5c5c033b093bb2fa3" ) self.baseurl: Text = "https://owner-api.teslamotors.com" + self.websocket_url: Text = "wss://streaming.vn.teslamotors.com/streaming" self.api: Text = "/api/1/" self.oauth: Dict[Text, Text] = {} self.expiration: int = 0 @@ -51,6 +53,7 @@ def __init__( if access_token: self.__sethead(access_token) _LOGGER.debug("Connecting with existing access token") + self.websocket = None def generate_oauth( self, email: Text = None, password: Text = None, refresh_token: Text = None @@ -149,3 +152,51 @@ async def __open( except aiohttp.ClientResponseError as exception_: raise TeslaException(exception_.status) return data + + async def websocket_connect(self, vin: int, vehicle_id: int, **kwargs): + """Connect to Tesla streaming websocket. + + Args: + vin (int): vin of vehicle + vehicle_id (int): vehicle_id from Tesla api + + """ + + async def _process_messages() -> None: + """Start Async WebSocket Listener.""" + async for msg in self.websocket: + _LOGGER.debug("msg: %s", msg) + if msg.type == aiohttp.WSMsgType.BINARY: + msg_json = json.loads(msg.data) + if msg_json["msg_type"] == "control:hello": + _LOGGER.debug( + "Succesfully connected to websocket %s for %s", + self.websocket_url, + vin[-5:], + ) + if ( + msg_json["msg_type"] == "data:error" + and msg_json["value"] == "Can't validate token. " + ): + raise TeslaException( + "Can't validate token for websocket connection." + ) + if kwargs.get("on_message"): + kwargs.get("on_message")(msg_json) + elif msg.type == aiohttp.WSMsgType.ERROR: + _LOGGER.debug("WSMsgType error") + break + + if not self.websocket or self.websocket.closed: + _LOGGER.debug("Connecting to websocket %s", self.websocket_url) + self.websocket = await self.websession.ws_connect(self.websocket_url) + loop = asyncio.get_event_loop() + loop.create_task(_process_messages()) + await self.websocket.send_json( + data={ + "msg_type": "data:subscribe_oauth", + "token": self.access_token, + "value": "speed,odometer,soc,elevation,est_heading,est_lat,est_lng,power,shift_state,range,est_range,heading", + "tag": f"{vehicle_id}", + } + ) diff --git a/teslajsonpy/controller.py b/teslajsonpy/controller.py index 3afb2a7f..2f8ddb77 100644 --- a/teslajsonpy/controller.py +++ b/teslajsonpy/controller.py @@ -74,6 +74,8 @@ def __init__( self.raw_online_state = {} self.__id_vin_map = {} self.__vin_id_map = {} + self.__vin_vehicle_id_map = {} + self.__vehicle_id_vin_map = {} async def connect(self, test_login=False) -> Tuple[Text, Text]: """Connect controller to Tesla.""" @@ -87,6 +89,8 @@ async def connect(self, test_login=False) -> Tuple[Text, Text]: vin = car["vin"] 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 @@ -474,6 +478,8 @@ async def update(self, car_id=None, wake_if_asleep=False, force=False): 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.raw_online_state[car["vin"]] = car["state"] self._last_attempted_update_time = cur_time @@ -520,6 +526,15 @@ async def update(self, car_id=None, wake_if_asleep=False, force=False): self.__gui[vin] = response["gui_settings"] self._last_update_time[vin] = time.time() update_succeeded = True + if ( + self.get_drive_params(car_id).get("shift_state") + and self.get_drive_params(car_id).get("shift_state") != "P" + ): + await self.__connection.websocket_connect( + vin[-5:], + self.__vin_vehicle_id_map[vin], + on_message=self._process_websocket_message, + ) return update_succeeded def get_climate_params(self, car_id): @@ -655,3 +670,46 @@ def _update_id(self, car_id: Text) -> Optional[Text]: 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] + _LOGGER.debug("Updating %s with websocket", vin[-5:]) + keys = [ + ("timestamp", int), + ("speed", int), + ("odometer", float), + ("soc", int), + ("elevation", int), + ("est_heading", int), + ("est_lat", float), + ("est_lng", float), + ("power", int), + ("shift_state", str), + ("range", int), + ("est_range", int), + ("heading", int), + ] + values = data["value"].split(",") + for num, value in enumerate(values): + update_json[keys[num][0]] = keys[num][1](value) if value else None + _LOGGER.debug("Update_json %s", update_json) + self.__driving[vin]["timestamp"] = update_json["timestamp"] + self.__charging[vin]["timestamp"] = update_json["timestamp"] + self.__state[vin]["timestamp"] = update_json["timestamp"] + self.__driving[vin]["speed"] = update_json["speed"] + self.__state[vin]["odometer"] = update_json["odometer"] + self.__charging[vin]["odometer"] = update_json["soc"] + # self.__state[vin]["odometer"] = update_json["elevation"] + # no current elevation stored + self.__driving[vin]["heading"] = update_json["est_heading"] + self.__driving[vin]["latitude"] = update_json["est_lat"] + self.__driving[vin]["longitude"] = update_json["est_lng"] + self.__driving[vin]["power"] = update_json["power"] + self.__driving[vin]["shift_state"] = update_json["shift_state"] + 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 From 91546aab04091cbfcf2f16bc35d938ec79e59178 Mon Sep 17 00:00:00 2001 From: "Alan D. Tse" Date: Tue, 28 Jan 2020 20:58:04 -0800 Subject: [PATCH 07/40] docs: update websocket_connect documentation --- teslajsonpy/connection.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/teslajsonpy/connection.py b/teslajsonpy/connection.py index ff7ff771..574c8cec 100644 --- a/teslajsonpy/connection.py +++ b/teslajsonpy/connection.py @@ -159,6 +159,8 @@ async def websocket_connect(self, vin: int, vehicle_id: int, **kwargs): Args: vin (int): vin of vehicle vehicle_id (int): vehicle_id from Tesla api + on_message (function): function to call on a valid message. It must + process a json delivered in data """ From a9e33c1af7109081097a726738c6fdb186f6cc7e Mon Sep 17 00:00:00 2001 From: "Alan D. Tse" Date: Tue, 28 Jan 2020 20:58:16 -0800 Subject: [PATCH 08/40] feat: add callback register for websocket --- teslajsonpy/controller.py | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/teslajsonpy/controller.py b/teslajsonpy/controller.py index 2f8ddb77..a9d69bcd 100644 --- a/teslajsonpy/controller.py +++ b/teslajsonpy/controller.py @@ -76,6 +76,7 @@ def __init__( self.__vin_id_map = {} self.__vin_vehicle_id_map = {} self.__vehicle_id_vin_map = {} + self.__websocket_listeners = [] async def connect(self, test_login=False) -> Tuple[Text, Text]: """Connect controller to Tesla.""" @@ -148,6 +149,19 @@ def get_tokens(self) -> Tuple[Text, Text]: self.__connection.token_refreshed = False return (self.__connection.refresh_token, self.__connection.access_token) + 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 + def wake_up(func): # pylint: disable=no-self-argument # issue is use of wraps on classmethods which should be replaced: @@ -676,7 +690,6 @@ def _process_websocket_message(self, data): update_json = {} vehicle_id = int(data["tag"]) vin = self.__vehicle_id_vin_map[vehicle_id] - _LOGGER.debug("Updating %s with websocket", vin[-5:]) keys = [ ("timestamp", int), ("speed", int), @@ -695,7 +708,7 @@ def _process_websocket_message(self, data): values = data["value"].split(",") for num, value in enumerate(values): update_json[keys[num][0]] = keys[num][1](value) if value else None - _LOGGER.debug("Update_json %s", update_json) + _LOGGER.debug("Updating %s with websocket: %s", vin[-5:], update_json) self.__driving[vin]["timestamp"] = update_json["timestamp"] self.__charging[vin]["timestamp"] = update_json["timestamp"] self.__state[vin]["timestamp"] = update_json["timestamp"] @@ -713,3 +726,5 @@ def _process_websocket_message(self, data): self.__charging[vin]["est_battery_range"] = update_json["est_range"] # self.__driving[vin]["heading"] = update_json["heading"] # est_heading appears more accurate + for func in self.__websocket_listeners: + func(data) From ac084353989330d3e3cdfab775aa04fe4363ce05 Mon Sep 17 00:00:00 2001 From: "Alan D. Tse" Date: Thu, 30 Jan 2020 22:10:52 -0800 Subject: [PATCH 09/40] refactor: migrate to non-legacy update url --- teslajsonpy/controller.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/teslajsonpy/controller.py b/teslajsonpy/controller.py index a9d69bcd..0303b738 100644 --- a/teslajsonpy/controller.py +++ b/teslajsonpy/controller.py @@ -526,7 +526,7 @@ async def update(self, car_id=None, wake_if_asleep=False, force=False): ): # Only update cars with update flag on try: data = await self.get( - car_id, "data", wake_if_asleep=wake_if_asleep + car_id, "vehicle_data", wake_if_asleep=wake_if_asleep ) except TeslaException: data = None From d7b1f5c9bf83935940cf0d7ef4d2cf0f04d6feda Mon Sep 17 00:00:00 2001 From: "Alan D. Tse" Date: Thu, 30 Jan 2020 22:21:40 -0800 Subject: [PATCH 10/40] feat: change to adaptive algorithm based updates Instead of polling for a set SCAN_INTERVAL, the new algorithm will determine if the car has recently parked and update normally for the IDLE_INTERVAL (600). After the idle period is complete, updates are throttled to the SLEEP_INTERVAL until the car is asleep. There is now a regular ONLINE_INTERVAL (60) check which does not query the car and will immediately detect if a car has become awake to resume updates. --- teslajsonpy/const.py | 10 +++++++++ teslajsonpy/controller.py | 44 +++++++++++++++++++++++++++++++++++++-- teslajsonpy/vehicle.py | 4 ++++ 3 files changed, 56 insertions(+), 2 deletions(-) create mode 100644 teslajsonpy/const.py diff --git a/teslajsonpy/const.py b/teslajsonpy/const.py new file mode 100644 index 00000000..eeb21783 --- /dev/null +++ b/teslajsonpy/const.py @@ -0,0 +1,10 @@ +# SPDX-License-Identifier: Apache-2.0 +""" +Python Package for controlling Tesla API. + +For more details about this api, please refer to the documentation at +https://github.com/zabuldon/teslajsonpy +""" +IDLE_INTERVAL = 600 # interval after parking to check at regular update_interval +ONLINE_INTERVAL = 60 # interval for checking online state; does not hit individual cars +SLEEP_INTERVAL = 660 # interval required to let vehicle sleep; based on testing diff --git a/teslajsonpy/controller.py b/teslajsonpy/controller.py index 0303b738..2b52b64b 100644 --- a/teslajsonpy/controller.py +++ b/teslajsonpy/controller.py @@ -20,6 +20,7 @@ from teslajsonpy.charger import ChargerSwitch, ChargingSensor, RangeSwitch from teslajsonpy.climate import Climate, TempSensor from teslajsonpy.connection import Connection +from teslajsonpy.const import IDLE_INTERVAL, ONLINE_INTERVAL, SLEEP_INTERVAL from teslajsonpy.exceptions import RetryLimitError, TeslaException from teslajsonpy.gps import GPS, Odometer from teslajsonpy.lock import ChargerLock, Lock @@ -77,6 +78,7 @@ def __init__( self.__vin_vehicle_id_map = {} self.__vehicle_id_vin_map = {} self.__websocket_listeners = [] + self.__last_parked_timestamp = {} async def connect(self, test_login=False) -> Tuple[Text, Text]: """Connect controller to Tesla.""" @@ -99,6 +101,7 @@ async def connect(self, test_login=False) -> Tuple[Text, Text]: self.__update[vin] = True self.raw_online_state[vin] = car["state"] 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] = {} @@ -460,6 +463,7 @@ async def _wake_up(self, car_id): return self.car_online[car_vin] async def update(self, car_id=None, wake_if_asleep=False, force=False): + # pylint: disable=too-many-locals """Update all vehicle attributes in the cache. This command will connect to the Tesla API and first update the list of @@ -482,11 +486,26 @@ async def update(self, car_id=None, wake_if_asleep=False, force=False): RetryLimitError """ + + def _calculate_next_interval(vin: int) -> int: + if self.raw_online_state[vin] == "asleep" or self.__driving[vin].get( + "shift_state" + ): + self.__last_parked_timestamp[vin] = cur_time + elif (cur_time - (self.__last_parked_timestamp[vin])) > IDLE_INTERVAL: + _LOGGER.debug( + "%s trying to sleep; will ignore updates for %s seconds", + vin[-5:], + round(SLEEP_INTERVAL + self._last_update_time[vin] - time.time(), 2), + ) + return SLEEP_INTERVAL + return self.update_interval + cur_time = time.time() async with self.__controller_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: + if force or cur_time - last_update > ONLINE_INTERVAL: cars = await self.get_vehicles() self.car_online = {} for car in cars: @@ -520,7 +539,7 @@ async def update(self, car_id=None, wake_if_asleep=False, force=False): or vin not in self._last_update_time or ( (cur_time - self._last_update_time[vin]) - > self.update_interval + > _calculate_next_interval(vin) ) ) ): # Only update cars with update flag on @@ -536,6 +555,18 @@ async def update(self, car_id=None, wake_if_asleep=False, force=False): 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() @@ -721,6 +752,15 @@ def _process_websocket_message(self, data): self.__driving[vin]["latitude"] = update_json["est_lat"] self.__driving[vin]["longitude"] = update_json["est_lng"] self.__driving[vin]["power"] = update_json["power"] + 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.__charging[vin]["battery_range"] = update_json["range"] self.__charging[vin]["est_battery_range"] = update_json["est_range"] diff --git a/teslajsonpy/vehicle.py b/teslajsonpy/vehicle.py index bcd0e7d6..12e2cd89 100644 --- a/teslajsonpy/vehicle.py +++ b/teslajsonpy/vehicle.py @@ -60,6 +60,10 @@ def id(self): """Return the id of this Vehicle.""" return self._id + def vehicle_id(self): + """Return the vehicle_id of this Vehicle.""" + return self._vehicle_id + def car_name(self): """Return the car name of this Vehicle.""" return ( From 890723838316c352573c9f7c1ced3dba667696c0 Mon Sep 17 00:00:00 2001 From: "Alan D. Tse" Date: Thu, 30 Jan 2020 22:50:19 -0800 Subject: [PATCH 11/40] fix: fix saving of websocket battery level --- teslajsonpy/controller.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/teslajsonpy/controller.py b/teslajsonpy/controller.py index 2b52b64b..1c48a743 100644 --- a/teslajsonpy/controller.py +++ b/teslajsonpy/controller.py @@ -745,7 +745,7 @@ def _process_websocket_message(self, data): self.__state[vin]["timestamp"] = update_json["timestamp"] self.__driving[vin]["speed"] = update_json["speed"] self.__state[vin]["odometer"] = update_json["odometer"] - self.__charging[vin]["odometer"] = update_json["soc"] + self.__charging[vin]["battery_level"] = update_json["soc"] # self.__state[vin]["odometer"] = update_json["elevation"] # no current elevation stored self.__driving[vin]["heading"] = update_json["est_heading"] From 9d0d6fd09032c00160996705dfecb1f471ea1738 Mon Sep 17 00:00:00 2001 From: "Alan D. Tse" Date: Thu, 30 Jan 2020 23:17:19 -0800 Subject: [PATCH 12/40] refactor: sleep only when sentry mode off --- teslajsonpy/controller.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/teslajsonpy/controller.py b/teslajsonpy/controller.py index 1c48a743..1b7a0e0b 100644 --- a/teslajsonpy/controller.py +++ b/teslajsonpy/controller.py @@ -492,11 +492,15 @@ def _calculate_next_interval(vin: int) -> int: "shift_state" ): self.__last_parked_timestamp[vin] = cur_time - elif (cur_time - (self.__last_parked_timestamp[vin])) > IDLE_INTERVAL: + elif ( + cur_time - (self.__last_parked_timestamp[vin]) + ) > IDLE_INTERVAL and not self.__state[vin].get("sentry_mode"): _LOGGER.debug( "%s trying to sleep; will ignore updates for %s seconds", vin[-5:], - round(SLEEP_INTERVAL + self._last_update_time[vin] - time.time(), 2), + round( + SLEEP_INTERVAL + self._last_update_time[vin] - time.time(), 2 + ), ) return SLEEP_INTERVAL return self.update_interval From 9739117f9bc1cb0dabd63b7ef8da9609206d9792 Mon Sep 17 00:00:00 2001 From: "Alan D. Tse" Date: Fri, 31 Jan 2020 00:22:53 -0800 Subject: [PATCH 13/40] refactor: move attributes to vehicle class --- teslajsonpy/binary_sensor.py | 3 --- teslajsonpy/vehicle.py | 2 ++ 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/teslajsonpy/binary_sensor.py b/teslajsonpy/binary_sensor.py index 88abf15d..b43f4c00 100644 --- a/teslajsonpy/binary_sensor.py +++ b/teslajsonpy/binary_sensor.py @@ -43,7 +43,6 @@ def __init__(self, data: Dict, controller): self.uniq_name = self._uniq_name() self.bin_type = 0x1 - self.attrs: Dict[Text, Text] = {} async def async_update(self): """Update the parking brake sensor.""" @@ -100,7 +99,6 @@ def __init__(self, data, controller): self.uniq_name = self._uniq_name() self.bin_type = 0x2 - self.attrs: Dict[Text, Text] = {} async def async_update(self): """Update the charger connection sensor.""" @@ -141,7 +139,6 @@ def __init__(self, data: Dict, controller) -> None: self.hass_type = "binary_sensor" self.name: Text = self._name() self.uniq_name: Text = self._uniq_name() - self.attrs: Dict[Text, Text] = {} async def async_update(self) -> None: """Update the battery state.""" diff --git a/teslajsonpy/vehicle.py b/teslajsonpy/vehicle.py index 12e2cd89..eb3a0f51 100644 --- a/teslajsonpy/vehicle.py +++ b/teslajsonpy/vehicle.py @@ -6,6 +6,7 @@ https://github.com/zabuldon/teslajsonpy """ import logging +from typing import Dict, Text _LOGGER = logging.getLogger(__name__) @@ -42,6 +43,7 @@ def __init__(self, data, controller): self._controller = controller self.should_poll = True self.type = "device" + self.attrs: Dict[Text, Text] = {} def _name(self): return ( From e0631e10fffba6f3d7180fa9e97fcad305eaa843 Mon Sep 17 00:00:00 2001 From: "Alan D. Tse" Date: Fri, 31 Jan 2020 00:24:15 -0800 Subject: [PATCH 14/40] refactor: add attributes to charger connection --- teslajsonpy/binary_sensor.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/teslajsonpy/binary_sensor.py b/teslajsonpy/binary_sensor.py index b43f4c00..f187b9bd 100644 --- a/teslajsonpy/binary_sensor.py +++ b/teslajsonpy/binary_sensor.py @@ -106,6 +106,10 @@ async def async_update(self): data = self._controller.get_charging_params(self._id) if data: self.attrs["charging_state"] = data["charging_state"] + self.attrs["conn_charge_cable"] = data["conn_charge_cable"] + self.attrs["fast_charger_present"] = data["fast_charger_present"] + self.attrs["fast_charger_brand"] = data["fast_charger_brand"] + self.attrs["fast_charger_type"] = data["fast_charger_type"] if data["charging_state"] in ["Disconnected"]: self.__state = False else: From 07c69e3d2ed002ecdd90b794adb0b87277661100 Mon Sep 17 00:00:00 2001 From: "Alan D. Tse" Date: Tue, 4 Feb 2020 19:43:47 -0800 Subject: [PATCH 15/40] feat: expose charger_phases --- teslajsonpy/charger.py | 1 + 1 file changed, 1 insertion(+) diff --git a/teslajsonpy/charger.py b/teslajsonpy/charger.py index 166ca493..e8dad333 100644 --- a/teslajsonpy/charger.py +++ b/teslajsonpy/charger.py @@ -170,6 +170,7 @@ async def async_update(self) -> None: self.__rated = data["gui_range_display"] == "Rated" data = self._controller.get_charging_params(self._id) if data: + self.attrs["charger_phases"] = data["charger_phases"] self.__added_range = ( data["charge_miles_added_rated"] if self.__rated From 5f1e34d351a5e9268924bfb591730752b7b01b25 Mon Sep 17 00:00:00 2001 From: "Alan D. Tse" Date: Tue, 4 Feb 2020 19:44:38 -0800 Subject: [PATCH 16/40] fix: further fine tune adaptive checking --- teslajsonpy/connection.py | 8 +++++ teslajsonpy/const.py | 1 + teslajsonpy/controller.py | 64 +++++++++++++++++++++++++++++++++------ 3 files changed, 63 insertions(+), 10 deletions(-) diff --git a/teslajsonpy/connection.py b/teslajsonpy/connection.py index 574c8cec..c2084efe 100644 --- a/teslajsonpy/connection.py +++ b/teslajsonpy/connection.py @@ -161,6 +161,8 @@ async def websocket_connect(self, vin: int, vehicle_id: int, **kwargs): vehicle_id (int): vehicle_id from Tesla api on_message (function): function to call on a valid message. It must process a json delivered in data + on_disconnect (function): function to call on a disconnect message. It must + process a json delivered in data """ @@ -183,6 +185,12 @@ async def _process_messages() -> None: raise TeslaException( "Can't validate token for websocket connection." ) + if ( + msg_json["msg_type"] == "data:error" + and msg_json["value"] == "disconnected" + ): + if kwargs.get("on_disconnect"): + kwargs.get("on_disconnect")() if kwargs.get("on_message"): kwargs.get("on_message")(msg_json) elif msg.type == aiohttp.WSMsgType.ERROR: diff --git a/teslajsonpy/const.py b/teslajsonpy/const.py index eeb21783..ffd4dd9f 100644 --- a/teslajsonpy/const.py +++ b/teslajsonpy/const.py @@ -8,3 +8,4 @@ IDLE_INTERVAL = 600 # interval after parking to check at regular update_interval ONLINE_INTERVAL = 60 # interval for checking online state; does not hit individual cars SLEEP_INTERVAL = 660 # interval required to let vehicle sleep; based on testing +DRIVING_INTERVAL = 60 # interval when driving detected diff --git a/teslajsonpy/controller.py b/teslajsonpy/controller.py index 1b7a0e0b..0a071f11 100644 --- a/teslajsonpy/controller.py +++ b/teslajsonpy/controller.py @@ -20,7 +20,12 @@ from teslajsonpy.charger import ChargerSwitch, ChargingSensor, RangeSwitch from teslajsonpy.climate import Climate, TempSensor from teslajsonpy.connection import Connection -from teslajsonpy.const import IDLE_INTERVAL, ONLINE_INTERVAL, SLEEP_INTERVAL +from teslajsonpy.const import ( + DRIVING_INTERVAL, + IDLE_INTERVAL, + ONLINE_INTERVAL, + SLEEP_INTERVAL, +) from teslajsonpy.exceptions import RetryLimitError, TeslaException from teslajsonpy.gps import GPS, Odometer from teslajsonpy.lock import ChargerLock, Lock @@ -79,6 +84,7 @@ def __init__( self.__vehicle_id_vin_map = {} self.__websocket_listeners = [] self.__last_parked_timestamp = {} + self.__update_state = {} async def connect(self, test_login=False) -> Tuple[Text, Text]: """Connect controller to Tesla.""" @@ -99,6 +105,7 @@ async def connect(self, test_login=False) -> Tuple[Text, Text]: self._last_update_time[vin] = 0 self._last_wake_up_time[vin] = 0 self.__update[vin] = True + self.__update_state[vin] = "normal" self.raw_online_state[vin] = car["state"] self.car_online[vin] = car["state"] == "online" self.__last_parked_timestamp[vin] = self._last_attempted_update_time @@ -463,7 +470,7 @@ async def _wake_up(self, car_id): return self.car_online[car_vin] async def update(self, car_id=None, wake_if_asleep=False, force=False): - # pylint: disable=too-many-locals + # 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 @@ -488,21 +495,58 @@ async def update(self, car_id=None, wake_if_asleep=False, force=False): """ def _calculate_next_interval(vin: int) -> 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.raw_online_state[vin] == "asleep" or self.__driving[vin].get( "shift_state" ): - self.__last_parked_timestamp[vin] = cur_time - elif ( - cur_time - (self.__last_parked_timestamp[vin]) - ) > IDLE_INTERVAL and not self.__state[vin].get("sentry_mode"): _LOGGER.debug( - "%s trying to sleep; will ignore updates for %s seconds", + "%s resetting last_parked_timestamp: shift_state %s", vin[-5:], - round( - SLEEP_INTERVAL + self._last_update_time[vin] - time.time(), 2 - ), + 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" + ): + 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 cur_time = time.time() From 990ee3d497d49110421592e52a3b3551a815eb1e Mon Sep 17 00:00:00 2001 From: "Alan D. Tse" Date: Sat, 8 Feb 2020 22:56:40 -0800 Subject: [PATCH 17/40] fix: fix handling of could_not_wake_buses --- teslajsonpy/controller.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/teslajsonpy/controller.py b/teslajsonpy/controller.py index 0a071f11..7ff59779 100644 --- a/teslajsonpy/controller.py +++ b/teslajsonpy/controller.py @@ -226,8 +226,8 @@ def valid_result(result): isinstance(result, dict) and isinstance(result["response"], dict) and ( - result["response"].get("result") is not False - or result["response"].get("result") + result["response"].get("result") is True + or result["response"].get("reason") != "could_not_wake_buses" ) ) From 2445d5b6a44a7b93fd11c8b52786c1cabd2af6a2 Mon Sep 17 00:00:00 2001 From: "Alan D. Tse" Date: Sat, 8 Feb 2020 23:09:51 -0800 Subject: [PATCH 18/40] refactor: save car_state from get_vehicles --- teslajsonpy/binary_sensor.py | 2 +- teslajsonpy/controller.py | 13 +++++++------ 2 files changed, 8 insertions(+), 7 deletions(-) diff --git a/teslajsonpy/binary_sensor.py b/teslajsonpy/binary_sensor.py index f187b9bd..e34ed7eb 100644 --- a/teslajsonpy/binary_sensor.py +++ b/teslajsonpy/binary_sensor.py @@ -148,7 +148,7 @@ async def async_update(self) -> None: """Update the battery state.""" await super().async_update() self.__online_state = self._controller.car_online[self._vin] - self.attrs["state"] = self._controller.raw_online_state[self._vin] + self.attrs["state"] = self._controller.car_state[self._vin].get("state") @staticmethod def has_battery() -> bool: diff --git a/teslajsonpy/controller.py b/teslajsonpy/controller.py index 7ff59779..5d43e3f6 100644 --- a/teslajsonpy/controller.py +++ b/teslajsonpy/controller.py @@ -77,7 +77,7 @@ def __init__( self.__controller_lock = None self.__wakeup_conds = {} self.car_online = {} - self.raw_online_state = {} + self.car_state = {} self.__id_vin_map = {} self.__vin_id_map = {} self.__vin_vehicle_id_map = {} @@ -106,7 +106,7 @@ async def connect(self, test_login=False) -> Tuple[Text, Text]: self._last_wake_up_time[vin] = 0 self.__update[vin] = True self.__update_state[vin] = "normal" - self.raw_online_state[vin] = car["state"] + 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] = {} @@ -463,9 +463,10 @@ async def _wake_up(self, car_id): 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:], result["response"]["state"] + "Wakeup %s: %s", car_vin[-5:], self.car_state[car_vin]["state"] ) return self.car_online[car_vin] @@ -506,7 +507,7 @@ def _calculate_next_interval(vin: int) -> int: # self.__climate[vin].get("is_climate_on"), # self.__charging[vin].get("charging_state") == "Charging", # ) - if self.raw_online_state[vin] == "asleep" or self.__driving[vin].get( + if self.car_state[vin].get("state") == "asleep" or self.__driving[vin].get( "shift_state" ): _LOGGER.debug( @@ -562,7 +563,7 @@ def _calculate_next_interval(vin: int) -> int: 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.raw_online_state[car["vin"]] = car["state"] + 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 @@ -576,7 +577,7 @@ def _calculate_next_interval(vin: int) -> int: if car_vin and car_vin != vin: continue async with self.__lock[vin]: - car_state = self.raw_online_state[vin] + car_state = self.car_state[vin].get("state") if ( (online or (wake_if_asleep and car_state == "asleep")) and ( # pylint: disable=too-many-boolean-expressions From cbe539e929c330bb6ded74d9392f55d09481dfbf Mon Sep 17 00:00:00 2001 From: "Alan D. Tse" Date: Sat, 8 Feb 2020 23:12:20 -0800 Subject: [PATCH 19/40] refactor: switch to backoff for retries --- Pipfile | 1 + Pipfile.lock | 471 +++++++++++++++++++++----------------- teslajsonpy/connection.py | 17 +- teslajsonpy/controller.py | 60 ++--- teslajsonpy/exceptions.py | 2 +- 5 files changed, 306 insertions(+), 245 deletions(-) diff --git a/Pipfile b/Pipfile index 57a4917b..79e1208f 100644 --- a/Pipfile +++ b/Pipfile @@ -16,6 +16,7 @@ black = "*" [packages] aiohttp = "*" +backoff = "*" [pipenv] allow_prereleases = true diff --git a/Pipfile.lock b/Pipfile.lock index a07dc7c6..28e37604 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "9e928f5ac4d78f7d58a53f07fc7ab43b7e1d6ebf0d11f665a094bd83d1e9ff54" + "sha256": "81182701cadd01dcf3eb5056bd5b37a42cdc2cfa46f7489e8df4d3650edca98c" }, "pipfile-spec": 6, "requires": {}, @@ -57,59 +57,55 @@ }, "multidict": { "hashes": [ - "sha256:024b8129695a952ebd93373e45b5d341dbb87c17ce49637b34000093f243dd4f", - "sha256:041e9442b11409be5e4fc8b6a97e4bcead758ab1e11768d1e69160bdde18acc3", - "sha256:045b4dd0e5f6121e6f314d81759abd2c257db4634260abcfe0d3f7083c4908ef", - "sha256:047c0a04e382ef8bd74b0de01407e8d8632d7d1b4db6f2561106af812a68741b", - "sha256:068167c2d7bbeebd359665ac4fff756be5ffac9cda02375b5c5a7c4777038e73", - "sha256:148ff60e0fffa2f5fad2eb25aae7bef23d8f3b8bdaf947a65cdbe84a978092bc", - "sha256:1d1c77013a259971a72ddaa83b9f42c80a93ff12df6a4723be99d858fa30bee3", - "sha256:1d48bc124a6b7a55006d97917f695effa9725d05abe8ee78fd60d6588b8344cd", - "sha256:31dfa2fc323097f8ad7acd41aa38d7c614dd1960ac6681745b6da124093dc351", - "sha256:34f82db7f80c49f38b032c5abb605c458bac997a6c3142e0d6c130be6fb2b941", - "sha256:3d5dd8e5998fb4ace04789d1d008e2bb532de501218519d70bb672c4c5a2fc5d", - "sha256:4a6ae52bd3ee41ee0f3acf4c60ceb3f44e0e3bc52ab7da1c2b2aa6703363a3d1", - "sha256:4b02a3b2a2f01d0490dd39321c74273fed0568568ea0e7ea23e02bd1fb10a10b", - "sha256:4b843f8e1dd6a3195679d9838eb4670222e8b8d01bc36c9894d6c3538316fa0a", - "sha256:5de53a28f40ef3c4fd57aeab6b590c2c663de87a5af76136ced519923d3efbb3", - "sha256:61b2b33ede821b94fa99ce0b09c9ece049c7067a33b279f343adfe35108a4ea7", - "sha256:6a3a9b0f45fd75dc05d8e93dc21b18fc1670135ec9544d1ad4acbcf6b86781d0", - "sha256:76ad8e4c69dadbb31bad17c16baee61c0d1a4a73bed2590b741b2e1a46d3edd0", - "sha256:7ba19b777dc00194d1b473180d4ca89a054dd18de27d0ee2e42a103ec9b7d014", - "sha256:7c1b7eab7a49aa96f3db1f716f0113a8a2e93c7375dd3d5d21c4941f1405c9c5", - "sha256:7fc0eee3046041387cbace9314926aa48b681202f8897f8bff3809967a049036", - "sha256:8ccd1c5fff1aa1427100ce188557fc31f1e0a383ad8ec42c559aabd4ff08802d", - "sha256:8e08dd76de80539d613654915a2f5196dbccc67448df291e69a88712ea21e24a", - "sha256:c18498c50c59263841862ea0501da9f2b3659c00db54abfbf823a80787fde8ce", - "sha256:c49db89d602c24928e68c0d510f4fcf8989d77defd01c973d6cbe27e684833b1", - "sha256:ce20044d0317649ddbb4e54dab3c1bcc7483c78c27d3f58ab3d0c7e6bc60d26a", - "sha256:d1071414dd06ca2eafa90c85a079169bfeb0e5f57fd0b45d44c092546fcd6fd9", - "sha256:d3be11ac43ab1a3e979dac80843b42226d5d3cccd3986f2e03152720a4297cd7", - "sha256:db603a1c235d110c860d5f39988ebc8218ee028f07a7cbc056ba6424372ca31b" - ], - "version": "==4.5.2" + "sha256:13f3ebdb5693944f52faa7b2065b751cb7e578b8dd0a5bb8e4ab05ad0188b85e", + "sha256:26502cefa86d79b86752e96639352c7247846515c864d7c2eb85d036752b643c", + "sha256:4fba5204d32d5c52439f88437d33ad14b5f228e25072a192453f658bddfe45a7", + "sha256:527124ef435f39a37b279653ad0238ff606b58328ca7989a6df372fd75d7fe26", + "sha256:5414f388ffd78c57e77bd253cf829373721f450613de53dc85a08e34d806e8eb", + "sha256:5eee66f882ab35674944dfa0d28b57fa51e160b4dce0ce19e47f495fdae70703", + "sha256:63810343ea07f5cd86ba66ab66706243a6f5af075eea50c01e39b4ad6bc3c57a", + "sha256:6bd10adf9f0d6a98ccc792ab6f83d18674775986ba9bacd376b643fe35633357", + "sha256:83c6ddf0add57c6b8a7de0bc7e2d656be3eefeff7c922af9a9aae7e49f225625", + "sha256:93166e0f5379cf6cd29746989f8a594fa7204dcae2e9335ddba39c870a287e1c", + "sha256:9a7b115ee0b9b92d10ebc246811d8f55d0c57e82dbb6a26b23c9a9a6ad40ce0c", + "sha256:a38baa3046cce174a07a59952c9f876ae8875ef3559709639c17fdf21f7b30dd", + "sha256:a6d219f49821f4b2c85c6d426346a5d84dab6daa6f85ca3da6c00ed05b54022d", + "sha256:a8ed33e8f9b67e3b592c56567135bb42e7e0e97417a4b6a771e60898dfd5182b", + "sha256:d7d428488c67b09b26928950a395e41cc72bb9c3d5abfe9f0521940ee4f796d4", + "sha256:dcfed56aa085b89d644af17442cdc2debaa73388feba4b8026446d168ca8dad7", + "sha256:f29b885e4903bd57a7789f09fe9d60b6475a6c1a4c0eca874d8558f00f9d4b51" + ], + "version": "==4.7.4" }, "typing-extensions": { "hashes": [ - "sha256:2ed632b30bb54fc3941c382decfd0ee4148f5c591651c9272473fea2c6397d95", - "sha256:b1edbbf0652660e32ae780ac9433f4231e7339c7f9a8057d0f042fcbcea49b87", - "sha256:d8179012ec2c620d3791ca6fe2bf7979d979acdbef1fca0bc56b37411db682ed" + "sha256:091ecc894d5e908ac75209f10d5b4f118fbdb2eb1ede6a63544054bb1edb41f2", + "sha256:910f4656f54de5993ad9304959ce9bb903f90aadc7c67a0bef07e678014e892d", + "sha256:cf8b63fedea4d89bab840ecbb93e75578af28f76f66c35889bd7065f5af88575" ], - "version": "==3.7.4" + "version": "==3.7.4.1" }, "yarl": { "hashes": [ - "sha256:2e5f4f4d709aed4f689843ef605fc432d0472e3325a8e165b1ecf1e2f3f22a89", - "sha256:412fe567284bdd16a8035c5a6f541da9872804b5702378532cf2e3ef5ae4a9e6", - "sha256:629f542dfd4e964c9e32039a1515b3262dae210f8802cfcceca9f0b529c68ee7", - "sha256:7bd0b31008afcba762d75171ac09135f49bc17c8d092bce49f0b9bf2c4b51850", - "sha256:9510699e48d7565a2c1dc0a9fcb396f1ac80802991621cf02d55540d571a5dc9", - "sha256:9a07c2a1b0c8b314ad5bcea37e62c109d8c19ad9f02985e8db898a56c129984f", - "sha256:9a49055f9e3121449c76709986aee829c74e2eee3b98185542799eec97808ed1", - "sha256:b59dd6679cb2ae172eb594f484e482aaac66fbff71a717603b0f9adf01649386", - "sha256:e5d6b530bec5817be9383452728c116d59868bfbed650ccc7657b2ec9aa51c03" - ], - "version": "==1.4.0a11" + "sha256:0c2ab325d33f1b824734b3ef51d4d54a54e0e7a23d13b86974507602334c2cce", + "sha256:0ca2f395591bbd85ddd50a82eb1fde9c1066fafe888c5c7cc1d810cf03fd3cc6", + "sha256:2098a4b4b9d75ee352807a95cdf5f10180db903bc5b7270715c6bbe2551f64ce", + "sha256:25e66e5e2007c7a39541ca13b559cd8ebc2ad8fe00ea94a2aad28a9b1e44e5ae", + "sha256:26d7c90cb04dee1665282a5d1a998defc1a9e012fdca0f33396f81508f49696d", + "sha256:308b98b0c8cd1dfef1a0311dc5e38ae8f9b58349226aa0533f15a16717ad702f", + "sha256:3ce3d4f7c6b69c4e4f0704b32eca8123b9c58ae91af740481aa57d7857b5e41b", + "sha256:58cd9c469eced558cd81aa3f484b2924e8897049e06889e8ff2510435b7ef74b", + "sha256:5b10eb0e7f044cf0b035112446b26a3a2946bca9d7d7edb5e54a2ad2f6652abb", + "sha256:6faa19d3824c21bcbfdfce5171e193c8b4ddafdf0ac3f129ccf0cdfcb083e462", + "sha256:944494be42fa630134bf907714d40207e646fd5a94423c90d5b514f7b0713fea", + "sha256:a161de7e50224e8e3de6e184707476b5a989037dcb24292b391a3d66ff158e70", + "sha256:a4844ebb2be14768f7994f2017f70aca39d658a96c786211be5ddbe1c68794c1", + "sha256:c2b509ac3d4b988ae8769901c66345425e361d518aecbe4acbfc2567e416626a", + "sha256:c9959d49a77b0e07559e579f38b2f3711c2b8716b8410b320bf9713013215a1b", + "sha256:d8cdee92bc930d8b09d8bd2043cedd544d9c8bd7436a77678dd602467a993080", + "sha256:e15199cdb423316e15f108f51249e44eb156ae5dba232cb73be555324a1d49c2" + ], + "version": "==1.4.2" } }, "develop": { @@ -122,17 +118,10 @@ }, "astroid": { "hashes": [ - "sha256:09a3fba616519311f1af8a461f804b68f0370e100c9264a035aa7846d7852e33", - "sha256:5a79c9b4bd6c4be777424593f957c996e20beb5f74e0bc332f47713c6f675efe" - ], - "version": "==2.3.2" - }, - "atomicwrites": { - "hashes": [ - "sha256:03472c30eb2c5d1ba9227e4c2ca66ab8287fbfbbda3888aa93dc2e28fc6811b4", - "sha256:75a9445bac02d8d058d5e1fe689654ba5a6556a1dfd8ce6ec55a0ed79866cfa6" + "sha256:71ea07f44df9568a75d0f354c49143a4575d90645e9fead6dfb52c26a85ed13a", + "sha256:840947ebfa8b58f318d42301cf8c0a20fd794a33b61cc4638e28e9e61ba32f42" ], - "version": "==1.3.0" + "version": "==2.3.3" }, "attrs": { "hashes": [ @@ -141,12 +130,19 @@ ], "version": "==19.3.0" }, + "backoff": { + "hashes": [ + "sha256:5e73e2cbe780e1915a204799dba0a01896f45f4385e636bcca7a0614d879d0cd", + "sha256:b8fba021fac74055ac05eb7c7bfce4723aedde6cd0a504e5326bcb0bdd6d19a4" + ], + "version": "==1.10.0" + }, "black": { "hashes": [ - "sha256:09a9dcb7c46ed496a9850b76e4e825d6049ecd38b611f1224857a79bd985a8cf", - "sha256:68950ffd4d9169716bcb8719a56c07a2f4485354fec061cdd5910aa07369731c" + "sha256:1b30e59be925fafc1ee4565e5e08abef6b03fe455102883820fe5ee2e4734e0b", + "sha256:c2edb73a08e9e0e6f65a0e6af18b059b8b1cdd5bef997d7a0b181df93dc81539" ], - "version": "==19.3b0" + "version": "==19.10b0" }, "bleach": { "hashes": [ @@ -157,10 +153,10 @@ }, "certifi": { "hashes": [ - "sha256:e4f3620cfea4f83eedc95b24abd9cd56f3c4b146dd0177e83a21b4eb49e21e50", - "sha256:fd7c7c74727ddcf00e9acd26bba8da604ffec95bf1c2144e67aff7a8b50e6cef" + "sha256:017c25db2a153ce562900032d5bc68e9f191e44e9a0f762f373977de9df1fbb3", + "sha256:25b64c7da4cd7479594d035c08c2d809eb4aab3a26e5a990ea98cc450c320f1f" ], - "version": "==2019.9.11" + "version": "==2019.11.28" }, "chardet": { "hashes": [ @@ -185,33 +181,39 @@ }, "coverage": { "hashes": [ - "sha256:17a417c691de3fc88de027832267313e5ed2b2ea3956745b562c4c389e44d05b", - "sha256:24307e67ebd9dc06fcbab9b7fef87412a97746c1baabb04ed8a93d5c2ccfe5ba", - "sha256:2a5d44a9d8426bd3699123864e63f008dc8dea9df22d5216a141a25d4670f22c", - "sha256:3726b8f5461e103a40e380f52b4b4ccdf2eda55d5d72f037cee43627992b4462", - "sha256:39dd15bbc4880a64399e180925bbc21c0c316a3065f6455d2512039f5cb59b94", - "sha256:3bb121f5dd156aab4fba2ebad6b0ad605bc5dc305931140dc614b101aa9d81ed", - "sha256:3bfdea9226eaed97736c973a7d6d0bbf9e1c1f1c7391c8e9c2bb2d0dbae49156", - "sha256:43be906a16239c1aa9f3742e3e6b0a5dd24781a13ce401f063262e9b4e93b69f", - "sha256:4a54cac1b39b2925041a41bcd1f191898fe401618627d7c3abf127c32a1c6dd1", - "sha256:4e58d65b90d6f26b3ccca7cf0fe573ef847347b8734af596a087a21eebb681f5", - "sha256:50229727d9baf0cd7f5ee6b194bf9dea708e9a20823d93f9e04d710b0a60e757", - "sha256:5141cdb010e9cd6939e37b8c2769d535cb535d80ef94f927c8a306f2e05a4736", - "sha256:748ba2b950425b9aef9d1bde2d6af7023585505016bd634e578f76ada4a30465", - "sha256:75e635bc6730c88b04421b25a0afc47b9b80efc1ed57630839196eb475722e50", - "sha256:78556f51dbfb33f18794eee29a4a8542fd2e301aa0d072653930793974dced03", - "sha256:7de17133509210ecc256535bab2f9a5547f3016c44f984fe12b4c10d81a4623f", - "sha256:83bf376555898fe2dc50d111a34b0152b504e454ed1e13cdcda6e5d50ba0ed5b", - "sha256:87730b5e4c3a42674fe8f0ecbb0d556c59c7e12b11a65c2178f2787252a80dfd", - "sha256:9bb7819c020c20c6200764879f0b10b323d6d4719aa7b0ae316c9e35730f9e2d", - "sha256:9c825788acb13d49ac20455433f3b862029aa497e97faba8c998555a042a6b91", - "sha256:b2bb4941c8838fc9ea2fca3c52e6dd865d39bbbc014bde249161bf8fcccf2152", - "sha256:c1b44c6c680f137910cb0f5481a2ae9899787ca7019f110a3708d9e99df941be", - "sha256:c52c2bc67bd3ff8db685f7c5f03e34a95bddd58a535630161f28d1c485d61e22", - "sha256:d6845e46338695c571759be1c770b013c477111e785b26151ec9feb6cd063543", - "sha256:e292b32dfc80d9f271af2d52df95455248322156e764763c4bfb2385b2e33533" - ], - "version": "==5.0a8" + "sha256:15cf13a6896048d6d947bf7d222f36e4809ab926894beb748fc9caa14605d9c3", + "sha256:1daa3eceed220f9fdb80d5ff950dd95112cd27f70d004c7918ca6dfc6c47054c", + "sha256:1e44a022500d944d42f94df76727ba3fc0a5c0b672c358b61067abb88caee7a0", + "sha256:25dbf1110d70bab68a74b4b9d74f30e99b177cde3388e07cc7272f2168bd1477", + "sha256:3230d1003eec018ad4a472d254991e34241e0bbd513e97a29727c7c2f637bd2a", + "sha256:3dbb72eaeea5763676a1a1efd9b427a048c97c39ed92e13336e726117d0b72bf", + "sha256:5012d3b8d5a500834783689a5d2292fe06ec75dc86ee1ccdad04b6f5bf231691", + "sha256:51bc7710b13a2ae0c726f69756cf7ffd4362f4ac36546e243136187cfcc8aa73", + "sha256:527b4f316e6bf7755082a783726da20671a0cc388b786a64417780b90565b987", + "sha256:722e4557c8039aad9592c6a4213db75da08c2cd9945320220634f637251c3894", + "sha256:76e2057e8ffba5472fd28a3a010431fd9e928885ff480cb278877c6e9943cc2e", + "sha256:77afca04240c40450c331fa796b3eab6f1e15c5ecf8bf2b8bee9706cd5452fef", + "sha256:7afad9835e7a651d3551eab18cbc0fdb888f0a6136169fbef0662d9cdc9987cf", + "sha256:9bea19ac2f08672636350f203db89382121c9c2ade85d945953ef3c8cf9d2a68", + "sha256:a8b8ac7876bc3598e43e2603f772d2353d9931709345ad6c1149009fd1bc81b8", + "sha256:b0840b45187699affd4c6588286d429cd79a99d509fe3de0f209594669bb0954", + "sha256:b26aaf69713e5674efbde4d728fb7124e429c9466aeaf5f4a7e9e699b12c9fe2", + "sha256:b63dd43f455ba878e5e9f80ba4f748c0a2156dde6e0e6e690310e24d6e8caf40", + "sha256:be18f4ae5a9e46edae3f329de2191747966a34a3d93046dbdf897319923923bc", + "sha256:c312e57847db2526bc92b9bfa78266bfbaabac3fdcd751df4d062cd4c23e46dc", + "sha256:c60097190fe9dc2b329a0eb03393e2e0829156a589bd732e70794c0dd804258e", + "sha256:c62a2143e1313944bf4a5ab34fd3b4be15367a02e9478b0ce800cb510e3bbb9d", + "sha256:cc1109f54a14d940b8512ee9f1c3975c181bbb200306c6d8b87d93376538782f", + "sha256:cd60f507c125ac0ad83f05803063bed27e50fa903b9c2cfee3f8a6867ca600fc", + "sha256:d513cc3db248e566e07a0da99c230aca3556d9b09ed02f420664e2da97eac301", + "sha256:d649dc0bcace6fcdb446ae02b98798a856593b19b637c1b9af8edadf2b150bea", + "sha256:d7008a6796095a79544f4da1ee49418901961c97ca9e9d44904205ff7d6aa8cb", + "sha256:da93027835164b8223e8e5af2cf902a4c80ed93cb0909417234f4a9df3bcd9af", + "sha256:e69215621707119c6baf99bda014a45b999d37602cb7043d943c76a59b05bf52", + "sha256:ea9525e0fef2de9208250d6c5aeeee0138921057cd67fcef90fbed49c4d62d37", + "sha256:fca1669d464f0c9831fd10be2eef6b86f5ebd76c724d1e0706ebdff86bb4adf0" + ], + "version": "==5.0.3" }, "detox": { "hashes": [ @@ -229,11 +231,10 @@ }, "docutils": { "hashes": [ - "sha256:6c4f696463b79f1fb8ba0c594b63840ebd41f059e92b31957c46b74a4599b6d0", - "sha256:9e4d7ecfc600058e07ba661411a2b7de2fd0fafa17d1a7f7361cd47b1175c827", - "sha256:a2aeea129088da402665e92e0b25b04b073c04b2dce4ab65caaa38b7ce2e1a99" + "sha256:0c5b78adfbf7762415433f5515cd5c9e762339e23369dbe8000d84a4bf4ab3af", + "sha256:c2de3a60e9e7d07be26b7f2b00ca0309c207e06c100f9cc2a94931fc75a478fc" ], - "version": "==0.15.2" + "version": "==0.16" }, "entrypoints": { "hashes": [ @@ -258,10 +259,10 @@ }, "flake8": { "hashes": [ - "sha256:19241c1cbc971b9962473e4438a2ca19749a7dd002dd1a946eaba171b4114548", - "sha256:8e9dfa3cecb2400b3738a42c54c3043e821682b9c840b0448c0503f781130696" + "sha256:45681a117ecc81e870cbf1262835ae4af5e7a8b08e40b944a8a6e6b895914cfb", + "sha256:49356e766643ad15072a789a20915d3c91dc89fd313ccd71802303fd67e4deca" ], - "version": "==3.7.8" + "version": "==3.7.9" }, "gitdb2": { "hashes": [ @@ -283,7 +284,9 @@ "sha256:0d48200bc50cbf498716712129eef819b1729339e34c3ae71656964dac907c28", "sha256:23d12eacffa9d0f290c0fe0c4e81ba6d5f3a5b7ac3c30a5eaf0126bf4deda5c8", "sha256:37c9ba82bd82eb6a23c2e5acc03055c0e45697253b2393c9a50cef76a3985304", + "sha256:51155342eb4d6058a0ffcd98a798fe6ba21195517da97e15fca3db12ab201e6e", "sha256:51503524dd6f152ab4ad1fbd168fc6c30b5795e8c70be4410a64940b3abb55c0", + "sha256:7457d685158522df483196b16ec648b28f8e847861adb01a55d41134e7734122", "sha256:8041e2de00e745c0e05a502d6e6db310db7faa7c979b3a5877123548a4c0b214", "sha256:81fcd96a275209ef117e9ec91f75c731fa18dcfd9ffaa1c0adbdaa3616a86043", "sha256:853da4f9563d982e4121fed8c92eea1a4594a2299037b3034c3c898cb8e933d6", @@ -297,7 +300,8 @@ "sha256:bcb530089ff24f6458a81ac3fa699e8c00194208a724b644ecc68422e1111939", "sha256:beeabe25c3b704f7d56b573f7d2ff88fc99f0138e43480cecdfcaa3b87fe4f87", "sha256:d634a7ea1fc3380ff96f9e44d8d22f38418c1c381d5fac680b272d7d90883720", - "sha256:d97b0661e1aead761f0ded3b769044bb00ed5d33e1ec865e891a8b128bf7c656" + "sha256:d97b0661e1aead761f0ded3b769044bb00ed5d33e1ec865e891a8b128bf7c656", + "sha256:e538b8dae561080b542b0f5af64d47ef859f22517f7eca617bb314e0e03fd7ef" ], "version": "==0.4.15" }, @@ -310,11 +314,11 @@ }, "importlib-metadata": { "hashes": [ - "sha256:aa18d7378b00b40847790e7c27e11673d7fed219354109d0e7b9e5b25dc3ad26", - "sha256:d5f18a79777f3aa179c145737780282e27b508fc8fd688cb17c7a813e8bd39af" + "sha256:06f5b3a99029c7134207dd882428a66992a9de2bef7c2b699b5641f9886c3302", + "sha256:b97607a1a18a5100839aec1dc26a1ea17ee0d93b20b0f008d80a5a050afb200b" ], "markers": "python_version < '3.8'", - "version": "==0.23" + "version": "==1.5.0" }, "invoke": { "hashes": [ @@ -332,26 +336,29 @@ }, "lazy-object-proxy": { "hashes": [ - "sha256:02b260c8deb80db09325b99edf62ae344ce9bc64d68b7a634410b8e9a568edbf", - "sha256:18f9c401083a4ba6e162355873f906315332ea7035803d0fd8166051e3d402e3", - "sha256:1f2c6209a8917c525c1e2b55a716135ca4658a3042b5122d4e3413a4030c26ce", - "sha256:2f06d97f0ca0f414f6b707c974aaf8829c2292c1c497642f63824119d770226f", - "sha256:616c94f8176808f4018b39f9638080ed86f96b55370b5a9463b2ee5c926f6c5f", - "sha256:63b91e30ef47ef68a30f0c3c278fbfe9822319c15f34b7538a829515b84ca2a0", - "sha256:77b454f03860b844f758c5d5c6e5f18d27de899a3db367f4af06bec2e6013a8e", - "sha256:83fe27ba321e4cfac466178606147d3c0aa18e8087507caec78ed5a966a64905", - "sha256:84742532d39f72df959d237912344d8a1764c2d03fe58beba96a87bfa11a76d8", - "sha256:874ebf3caaf55a020aeb08acead813baf5a305927a71ce88c9377970fe7ad3c2", - "sha256:9f5caf2c7436d44f3cec97c2fa7791f8a675170badbfa86e1992ca1b84c37009", - "sha256:a0c8758d01fcdfe7ae8e4b4017b13552efa7f1197dd7358dc9da0576f9d0328a", - "sha256:a4def978d9d28cda2d960c279318d46b327632686d82b4917516c36d4c274512", - "sha256:ad4f4be843dace866af5fc142509e9b9817ca0c59342fdb176ab6ad552c927f5", - "sha256:ae33dd198f772f714420c5ab698ff05ff900150486c648d29951e9c70694338e", - "sha256:b4a2b782b8a8c5522ad35c93e04d60e2ba7f7dcb9271ec8e8c3e08239be6c7b4", - "sha256:c462eb33f6abca3b34cdedbe84d761f31a60b814e173b98ede3c81bb48967c4f", - "sha256:fd135b8d35dfdcdb984828c84d695937e58cc5f49e1c854eb311c4d6aa03f4f1" + "sha256:0c4b206227a8097f05c4dbdd323c50edf81f15db3b8dc064d08c62d37e1a504d", + "sha256:194d092e6f246b906e8f70884e620e459fc54db3259e60cf69a4d66c3fda3449", + "sha256:1be7e4c9f96948003609aa6c974ae59830a6baecc5376c25c92d7d697e684c08", + "sha256:4677f594e474c91da97f489fea5b7daa17b5517190899cf213697e48d3902f5a", + "sha256:48dab84ebd4831077b150572aec802f303117c8cc5c871e182447281ebf3ac50", + "sha256:5541cada25cd173702dbd99f8e22434105456314462326f06dba3e180f203dfd", + "sha256:59f79fef100b09564bc2df42ea2d8d21a64fdcda64979c0fa3db7bdaabaf6239", + "sha256:8d859b89baf8ef7f8bc6b00aa20316483d67f0b1cbf422f5b4dc56701c8f2ffb", + "sha256:9254f4358b9b541e3441b007a0ea0764b9d056afdeafc1a5569eee1cc6c1b9ea", + "sha256:9651375199045a358eb6741df3e02a651e0330be090b3bc79f6d0de31a80ec3e", + "sha256:97bb5884f6f1cdce0099f86b907aa41c970c3c672ac8b9c8352789e103cf3156", + "sha256:9b15f3f4c0f35727d3a0fba4b770b3c4ebbb1fa907dbcc046a1d2799f3edd142", + "sha256:a2238e9d1bb71a56cd710611a1614d1194dc10a175c1e08d75e1a7bcc250d442", + "sha256:a6ae12d08c0bf9909ce12385803a543bfe99b95fe01e752536a60af2b7797c62", + "sha256:ca0a928a3ddbc5725be2dd1cf895ec0a254798915fb3a36af0964a0a4149e3db", + "sha256:cb2c7c57005a6804ab66f106ceb8482da55f5314b7fcb06551db1edae4ad1531", + "sha256:d74bb8693bf9cf75ac3b47a54d716bbb1a92648d5f781fc799347cfc95952383", + "sha256:d945239a5639b3ff35b70a88c5f2f491913eb94871780ebfabb2568bd58afc5a", + "sha256:eba7011090323c1dadf18b3b689845fd96a61ba0a1dfbd7f24b921398affc357", + "sha256:efa1909120ce98bbb3777e8b6f92237f5d5c8ea6758efea36a473e1d38f7d3e4", + "sha256:f3900e8a5de27447acbf900b4750b0ddfd7ec1ea7fbaf11dfa911141bc522af0" ], - "version": "==1.4.2" + "version": "==1.4.3" }, "mccabe": { "hashes": [ @@ -369,29 +376,29 @@ }, "more-itertools": { "hashes": [ - "sha256:409cd48d4db7052af495b09dec721011634af3753ae1ef92d2b32f73a745f832", - "sha256:92b8c4b06dac4f0611c0729b2f2ede52b2e1bac1ab48f089c7ddc12e26bb60c4" + "sha256:5dd8bcf33e5f9513ffa06d5ad33d78f31e1931ac9a18f33d37e77a180d393a7c", + "sha256:b1ddb932186d8a6ac451e1d95844b382f55e12686d51ca0c68b6f61f2ab7a507" ], - "version": "==7.2.0" + "version": "==8.2.0" }, "mypy": { "hashes": [ - "sha256:1521c186a3d200c399bd5573c828ea2db1362af7209b2adb1bb8532cea2fb36f", - "sha256:31a046ab040a84a0fc38bc93694876398e62bc9f35eca8ccbf6418b7297f4c00", - "sha256:3b1a411909c84b2ae9b8283b58b48541654b918e8513c20a400bb946aa9111ae", - "sha256:48c8bc99380575deb39f5d3400ebb6a8a1cb5cc669bbba4d3bb30f904e0a0e7d", - "sha256:540c9caa57a22d0d5d3c69047cc9dd0094d49782603eb03069821b41f9e970e9", - "sha256:672e418425d957e276c291930a3921b4a6413204f53fe7c37cad7bc57b9a3391", - "sha256:6ed3b9b3fdc7193ea7aca6f3c20549b377a56f28769783a8f27191903a54170f", - "sha256:9371290aa2cad5ad133e4cdc43892778efd13293406f7340b9ffe99d5ec7c1d9", - "sha256:ace6ac1d0f87d4072f05b5468a084a45b4eda970e4d26704f201e06d47ab2990", - "sha256:b428f883d2b3fe1d052c630642cc6afddd07d5cd7873da948644508be3b9d4a7", - "sha256:d5bf0e6ec8ba346a2cf35cb55bf4adfddbc6b6576fcc9e10863daa523e418dbb", - "sha256:d7574e283f83c08501607586b3167728c58e8442947e027d2d4c7dcd6d82f453", - "sha256:dc889c84241a857c263a2b1cd1121507db7d5b5f5e87e77147097230f374d10b", - "sha256:f4748697b349f373002656bf32fede706a0e713d67bfdcf04edf39b1f61d46eb" - ], - "version": "==0.740" + "sha256:0a9a45157e532da06fe56adcfef8a74629566b607fa2c1ac0122d1ff995c748a", + "sha256:2c35cae79ceb20d47facfad51f952df16c2ae9f45db6cb38405a3da1cf8fc0a7", + "sha256:4b9365ade157794cef9685791032521233729cb00ce76b0ddc78749abea463d2", + "sha256:53ea810ae3f83f9c9b452582261ea859828a9ed666f2e1ca840300b69322c474", + "sha256:634aef60b4ff0f650d3e59d4374626ca6153fcaff96ec075b215b568e6ee3cb0", + "sha256:7e396ce53cacd5596ff6d191b47ab0ea18f8e0ec04e15d69728d530e86d4c217", + "sha256:7eadc91af8270455e0d73565b8964da1642fe226665dd5c9560067cd64d56749", + "sha256:7f672d02fffcbace4db2b05369142e0506cdcde20cea0e07c7c2171c4fd11dd6", + "sha256:85baab8d74ec601e86134afe2bcccd87820f79d2f8d5798c889507d1088287bf", + "sha256:87c556fb85d709dacd4b4cb6167eecc5bbb4f0a9864b69136a0d4640fdc76a36", + "sha256:a6bd44efee4dc8c3324c13785a9dc3519b3ee3a92cada42d2b57762b7053b49b", + "sha256:c6d27bd20c3ba60d5b02f20bd28e20091d6286a699174dfad515636cb09b5a72", + "sha256:e2bb577d10d09a2d8822a042a23b8d62bc3b269667c9eb8e60a6edfa000211b1", + "sha256:f97a605d7c8bc2c6d1172c2f0d5a65b24142e11a58de689046e62c2d632ca8c1" + ], + "version": "==0.761" }, "mypy-extensions": { "hashes": [ @@ -409,10 +416,17 @@ }, "packaging": { "hashes": [ - "sha256:28b924174df7a2fa32c1953825ff29c61e2f5e082343165438812f00d3a7fc47", - "sha256:d9551545c6d761f3def1677baf08ab2a3ca17c56879e70fecba2fc4dde4ed108" + "sha256:170748228214b70b672c581a3dd610ee51f733018650740e98c7df862a583f73", + "sha256:e665345f9eef0c621aa0bf2f8d78cf6d21904eef16a93f020240b704a57f1334" + ], + "version": "==20.1" + }, + "pathspec": { + "hashes": [ + "sha256:163b0632d4e31cef212976cf57b43d9fd6b0bac6e67c26015d611a647d5e7424", + "sha256:562aa70af2e0d434367d9790ad37aed893de47f1693e4201fd1d3dca15d19b96" ], - "version": "==19.2" + "version": "==0.7.0" }, "pkginfo": { "hashes": [ @@ -423,17 +437,17 @@ }, "pluggy": { "hashes": [ - "sha256:0db4b7601aae1d35b4a033282da476845aa19185c1e6964b25cf324b5e4ec3e6", - "sha256:fa5fa1622fa6dd5c030e9cad086fa19ef6a0cf6d7a2d12318e10cb49d6d68f34" + "sha256:15b2acde666561e1298d71b523007ed7364de07029219b604cf808bfa1c765b0", + "sha256:966c145cd83c96502c3c3868f50408687b38434af77734af1e9ca461a4081d2d" ], - "version": "==0.13.0" + "version": "==0.13.1" }, "py": { "hashes": [ - "sha256:64f65755aee5b381cea27766a3a147c3f15b9b6b9ac88676de66ba2ae36793fa", - "sha256:dc639b046a6e2cff5bbe40194ad65936d6ba360b52b3c3fe1d08a82dd50b5e53" + "sha256:5e27081401262157467ad6e7f851b7aa402c5852dbcb3dae06768434de5752aa", + "sha256:c20fdd83a5dbc0af9efd622bee9a5564e278f6380fffcacc43ba6f43db2813b0" ], - "version": "==1.8.0" + "version": "==1.8.1" }, "pycodestyle": { "hashes": [ @@ -444,10 +458,10 @@ }, "pydocstyle": { "hashes": [ - "sha256:04c84e034ebb56eb6396c820442b8c4499ac5eb94a3bda88951ac3dc519b6058", - "sha256:66aff87ffe34b1e49bff2dd03a88ce6843be2f3346b0c9814410d34987fbab59" + "sha256:da7831660b7355307b32778c4a0dbfb137d89254ef31a2b2978f50fc0b4d7586", + "sha256:f4f5d210610c2d153fae39093d44224c17429e2ad7da12a8b419aba5c2f614b5" ], - "version": "==4.0.1" + "version": "==5.0.2" }, "pyflakes": { "hashes": [ @@ -458,31 +472,31 @@ }, "pygments": { "hashes": [ - "sha256:71e430bc85c88a430f000ac1d9b331d2407f681d6f6aec95e8bcfbc3df5b0127", - "sha256:881c4c157e45f30af185c1ffe8d549d48ac9127433f2c380c24b84572ad66297" + "sha256:2a3fe295e54a20164a9df49c75fa58526d3be48e14aceba6d6b1e8ac0bfd6f1b", + "sha256:98c8aa5a9f778fcd1026a17361ddaf7330d1b7c62ae97c3bb0ae73e0b9b6b0fe" ], - "version": "==2.4.2" + "version": "==2.5.2" }, "pylint": { "hashes": [ - "sha256:7b76045426c650d2b0f02fc47c14d7934d17898779da95288a74c2a7ec440702", - "sha256:856476331f3e26598017290fd65bebe81c960e806776f324093a46b76fb2d1c0" + "sha256:3db5468ad013380e987410a8d6956226963aed94ecb5f9d3a28acca6d9ac36cd", + "sha256:886e6afc935ea2590b462664b161ca9a5e40168ea99e5300935f6591ad467df4" ], - "version": "==2.4.3" + "version": "==2.4.4" }, "pyparsing": { "hashes": [ - "sha256:6f98a7b9397e206d78cc01df10131398f1c8b8510a2f4d97d9abd82e1aacdd80", - "sha256:d9338df12903bbf5d65a0e4e87c2161968b10d2e489652bb47001d82a9b028b4" + "sha256:4c830582a84fb022400b85429791bc551f1f4871c33f23e44f353119e92f969f", + "sha256:c342dccb5250c08d45fd6f8b4a559613ca603b57498511740e65cd11a2e7dcec" ], - "version": "==2.4.2" + "version": "==2.4.6" }, "pytest": { "hashes": [ - "sha256:7e4800063ccfc306a53c461442526c5571e1462f61583506ce97e4da6a1d88c8", - "sha256:ca563435f4941d0cb34767301c27bc65c510cb82e90b9ecf9cb52dc2c63caaa0" + "sha256:0d5fe9189a148acc3c3eb2ac8e1ac0742cb7618c084f3d228baaec0c254b318d", + "sha256:ff615c761e25eb25df19edddc0b970302d2a9091fbce0e7213298d85fb61fef6" ], - "version": "==5.2.1" + "version": "==5.3.5" }, "pytest-cov": { "hashes": [ @@ -500,10 +514,10 @@ }, "python-semantic-release": { "hashes": [ - "sha256:4a792ae6671d7771354fc5f5b015e449fc926e4ebf3328ef0caeec64a1ddb0ce", - "sha256:7a7707f49220f4b7f975dc4046c761936e1610e8145278f6716a638e8b29cdb6" + "sha256:038f8961b617906db72634ef0746515b5de17e09cd3a69a6b0a1c020e6b5edb3", + "sha256:e00fae90765a380c2d619fdf0ad9c940a4b8b769bec964845c03edb9ca73af7f" ], - "version": "==4.3.2" + "version": "==4.4.0" }, "readme-renderer": { "hashes": [ @@ -512,6 +526,32 @@ ], "version": "==24.0" }, + "regex": { + "hashes": [ + "sha256:07b39bf943d3d2fe63d46281d8504f8df0ff3fe4c57e13d1656737950e53e525", + "sha256:0932941cdfb3afcbc26cc3bcf7c3f3d73d5a9b9c56955d432dbf8bbc147d4c5b", + "sha256:0e182d2f097ea8549a249040922fa2b92ae28be4be4895933e369a525ba36576", + "sha256:10671601ee06cf4dc1bc0b4805309040bb34c9af423c12c379c83d7895622bb5", + "sha256:23e2c2c0ff50f44877f64780b815b8fd2e003cda9ce817a7fd00dea5600c84a0", + "sha256:26ff99c980f53b3191d8931b199b29d6787c059f2e029b2b0c694343b1708c35", + "sha256:27429b8d74ba683484a06b260b7bb00f312e7c757792628ea251afdbf1434003", + "sha256:3e77409b678b21a056415da3a56abfd7c3ad03da71f3051bbcdb68cf44d3c34d", + "sha256:4e8f02d3d72ca94efc8396f8036c0d3bcc812aefc28ec70f35bb888c74a25161", + "sha256:4eae742636aec40cf7ab98171ab9400393360b97e8f9da67b1867a9ee0889b26", + "sha256:6a6ae17bf8f2d82d1e8858a47757ce389b880083c4ff2498dba17c56e6c103b9", + "sha256:6a6ba91b94427cd49cd27764679024b14a96874e0dc638ae6bdd4b1a3ce97be1", + "sha256:7bcd322935377abcc79bfe5b63c44abd0b29387f267791d566bbb566edfdd146", + "sha256:98b8ed7bb2155e2cbb8b76f627b2fd12cf4b22ab6e14873e8641f266e0fb6d8f", + "sha256:bd25bb7980917e4e70ccccd7e3b5740614f1c408a642c245019cff9d7d1b6149", + "sha256:d0f424328f9822b0323b3b6f2e4b9c90960b24743d220763c7f07071e0778351", + "sha256:d58e4606da2a41659c84baeb3cfa2e4c87a74cec89a1e7c56bee4b956f9d7461", + "sha256:e3cd21cc2840ca67de0bbe4071f79f031c81418deb544ceda93ad75ca1ee9f7b", + "sha256:e6c02171d62ed6972ca8631f6f34fa3281d51db8b326ee397b9c83093a6b7242", + "sha256:e7c7661f7276507bce416eaae22040fd91ca471b5b33c13f8ff21137ed6f248c", + "sha256:ecc6de77df3ef68fee966bb8cb4e067e84d4d1f397d0ef6fce46913663540d77" + ], + "version": "==2020.1.8" + }, "requests": { "hashes": [ "sha256:11e007a8a2aa0323f5a921e9e6a2d7e4e67d9877e85773fba9ba6419025cbeb4", @@ -528,17 +568,17 @@ }, "semver": { "hashes": [ - "sha256:41c9aa26c67dc16c54be13074c352ab666bce1fa219c7110e8f03374cd4206b0", - "sha256:5b09010a66d9a3837211bb7ae5a20d10ba88f8cb49e92cb139a69ef90d5060d8" + "sha256:aa1c6be3bf23e346e00c509a7ee87735a7e0fd6b404cf066037cfeab2c770320", + "sha256:ed1edeaa0c27f68feb74f09f715077fd07b728446dc2bb7fc470fc0f737873a0" ], - "version": "==2.8.1" + "version": "==2.9.0" }, "six": { "hashes": [ - "sha256:3350809f0555b11f552448330d0b52d5f24c91a322ea4a15ef22629740f3761c", - "sha256:d16a0141ec1a18405cd4ce8b4613101da75da0e9a7aec5bdd4fa804d0e0eba73" + "sha256:236bdbdce46e6e6a3d61a337c0f8b763ca1e8717c03b369e87a7ec7ce1319c0a", + "sha256:8f3cd2e254d8f793e7f3d6d9df77b92252b52637291d0f0da013c76ea2724b6c" ], - "version": "==1.12.0" + "version": "==1.14.0" }, "smmap2": { "hashes": [ @@ -570,10 +610,10 @@ }, "tqdm": { "hashes": [ - "sha256:abc25d0ce2397d070ef07d8c7e706aede7920da163c64997585d42d3537ece3d", - "sha256:dd3fcca8488bb1d416aa7469d2f277902f26260c45aa86b667b074cd44b3b115" + "sha256:251ee8440dbda126b8dfa8a7c028eb3f13704898caaef7caa699b35e119301e2", + "sha256:fe231261cfcbc6f4a99165455f8f6b9ef4e1032a6e29bccf168b4bf42012f09c" ], - "version": "==4.36.1" + "version": "==4.42.1" }, "twine": { "hashes": [ @@ -584,58 +624,59 @@ }, "typed-ast": { "hashes": [ - "sha256:1170afa46a3799e18b4c977777ce137bb53c7485379d9706af8a59f2ea1aa161", - "sha256:18511a0b3e7922276346bcb47e2ef9f38fb90fd31cb9223eed42c85d1312344e", - "sha256:262c247a82d005e43b5b7f69aff746370538e176131c32dda9cb0f324d27141e", - "sha256:2b907eb046d049bcd9892e3076c7a6456c93a25bebfe554e931620c90e6a25b0", - "sha256:354c16e5babd09f5cb0ee000d54cfa38401d8b8891eefa878ac772f827181a3c", - "sha256:48e5b1e71f25cfdef98b013263a88d7145879fbb2d5185f2a0c79fa7ebbeae47", - "sha256:4e0b70c6fc4d010f8107726af5fd37921b666f5b31d9331f0bd24ad9a088e631", - "sha256:630968c5cdee51a11c05a30453f8cd65e0cc1d2ad0d9192819df9978984529f4", - "sha256:66480f95b8167c9c5c5c87f32cf437d585937970f3fc24386f313a4c97b44e34", - "sha256:71211d26ffd12d63a83e079ff258ac9d56a1376a25bc80b1cdcdf601b855b90b", - "sha256:7954560051331d003b4e2b3eb822d9dd2e376fa4f6d98fee32f452f52dd6ebb2", - "sha256:838997f4310012cf2e1ad3803bce2f3402e9ffb71ded61b5ee22617b3a7f6b6e", - "sha256:95bd11af7eafc16e829af2d3df510cecfd4387f6453355188342c3e79a2ec87a", - "sha256:bc6c7d3fa1325a0c6613512a093bc2a2a15aeec350451cbdf9e1d4bffe3e3233", - "sha256:cc34a6f5b426748a507dd5d1de4c1978f2eb5626d51326e43280941206c209e1", - "sha256:d755f03c1e4a51e9b24d899561fec4ccaf51f210d52abdf8c07ee2849b212a36", - "sha256:d7c45933b1bdfaf9f36c579671fec15d25b06c8398f113dab64c18ed1adda01d", - "sha256:d896919306dd0aa22d0132f62a1b78d11aaf4c9fc5b3410d3c666b818191630a", - "sha256:fdc1c9bbf79510b76408840e009ed65958feba92a88833cdceecff93ae8fff66", - "sha256:ffde2fbfad571af120fcbfbbc61c72469e72f550d676c3342492a9dfdefb8f12" + "sha256:0666aa36131496aed8f7be0410ff974562ab7eeac11ef351def9ea6fa28f6355", + "sha256:0c2c07682d61a629b68433afb159376e24e5b2fd4641d35424e462169c0a7919", + "sha256:249862707802d40f7f29f6e1aad8d84b5aa9e44552d2cc17384b209f091276aa", + "sha256:24995c843eb0ad11a4527b026b4dde3da70e1f2d8806c99b7b4a7cf491612652", + "sha256:269151951236b0f9a6f04015a9004084a5ab0d5f19b57de779f908621e7d8b75", + "sha256:4083861b0aa07990b619bd7ddc365eb7fa4b817e99cf5f8d9cf21a42780f6e01", + "sha256:498b0f36cc7054c1fead3d7fc59d2150f4d5c6c56ba7fb150c013fbc683a8d2d", + "sha256:4e3e5da80ccbebfff202a67bf900d081906c358ccc3d5e3c8aea42fdfdfd51c1", + "sha256:6daac9731f172c2a22ade6ed0c00197ee7cc1221aa84cfdf9c31defeb059a907", + "sha256:715ff2f2df46121071622063fc7543d9b1fd19ebfc4f5c8895af64a77a8c852c", + "sha256:73d785a950fc82dd2a25897d525d003f6378d1cb23ab305578394694202a58c3", + "sha256:8c8aaad94455178e3187ab22c8b01a3837f8ee50e09cf31f1ba129eb293ec30b", + "sha256:8ce678dbaf790dbdb3eba24056d5364fb45944f33553dd5869b7580cdbb83614", + "sha256:aaee9905aee35ba5905cfb3c62f3e83b3bec7b39413f0a7f19be4e547ea01ebb", + "sha256:bcd3b13b56ea479b3650b82cabd6b5343a625b0ced5429e4ccad28a8973f301b", + "sha256:c9e348e02e4d2b4a8b2eedb48210430658df6951fa484e59de33ff773fbd4b41", + "sha256:d205b1b46085271b4e15f670058ce182bd1199e56b317bf2ec004b6a44f911f6", + "sha256:d43943ef777f9a1c42bf4e552ba23ac77a6351de620aa9acf64ad54933ad4d34", + "sha256:d5d33e9e7af3b34a40dc05f498939f0ebf187f07c385fd58d591c533ad8562fe", + "sha256:fc0fea399acb12edbf8a628ba8d2312f583bdbdb3335635db062fa98cf71fca4", + "sha256:fe460b922ec15dd205595c9b5b99e2f056fd98ae8f9f56b888e7a17dc2b757e7" ], "markers": "implementation_name == 'cpython' and python_version < '3.8'", - "version": "==1.4.0" + "version": "==1.4.1" }, "typing-extensions": { "hashes": [ - "sha256:2ed632b30bb54fc3941c382decfd0ee4148f5c591651c9272473fea2c6397d95", - "sha256:b1edbbf0652660e32ae780ac9433f4231e7339c7f9a8057d0f042fcbcea49b87", - "sha256:d8179012ec2c620d3791ca6fe2bf7979d979acdbef1fca0bc56b37411db682ed" + "sha256:091ecc894d5e908ac75209f10d5b4f118fbdb2eb1ede6a63544054bb1edb41f2", + "sha256:910f4656f54de5993ad9304959ce9bb903f90aadc7c67a0bef07e678014e892d", + "sha256:cf8b63fedea4d89bab840ecbb93e75578af28f76f66c35889bd7065f5af88575" ], - "version": "==3.7.4" + "version": "==3.7.4.1" }, "urllib3": { "hashes": [ - "sha256:3de946ffbed6e6746608990594d08faac602528ac7015ac28d33cee6a45b7398", - "sha256:9a107b99a5393caf59c7aa3c1249c16e6879447533d0887f4336dde834c7be86" + "sha256:2f3db8b19923a873b3e5256dc9c2dedfa883e33d87c690d9c7913e1f40673cdc", + "sha256:87716c2d2a7121198ebcb7ce7cccf6ce5e9ba539041cfbaeecfb641dc0bf6acc" ], - "version": "==1.25.6" + "version": "==1.25.8" }, "virtualenv": { "hashes": [ - "sha256:11cb4608930d5fd3afb545ecf8db83fa50e1f96fc4fca80c94b07d2c83146589", - "sha256:d257bb3773e48cac60e475a19b608996c73f4d333b3ba2e4e57d5ac6134e0136" + "sha256:a7a7f272882815c2f84b9a5bfd75ab4e32eea257bc3169a9c139310c064ebbeb", + "sha256:d98aa9ae72aa2f892e697e38c0314cd835c8f44d7f38b2fe27d11a6aa084dd2c" ], - "version": "==16.7.7" + "version": "==20.0.0b2" }, "wcwidth": { "hashes": [ - "sha256:3df37372226d6e63e1b1e1eda15c594bca98a22d33a23832a90998faa96bc65e", - "sha256:f4ebe71925af7b40a864553f761ed559b43544f8f71746c2d756c7fe788ade7c" + "sha256:8fd29383f539be45b20bd4df0dc29c20ba48654a41e661925e612311e9f3c603", + "sha256:f28b3e8a6483e5d49e7f8949ac1a78314e740333ae305b4ba5defd3e74fb37a8" ], - "version": "==0.1.7" + "version": "==0.1.8" }, "webencodings": { "hashes": [ @@ -646,10 +687,10 @@ }, "wheel": { "hashes": [ - "sha256:10c9da68765315ed98850f8e048347c3eb06dd81822dc2ab1d4fde9dc9702646", - "sha256:f4da1763d3becf2e2cd92a14a7c920f0f00eca30fdde9ea992c836685b9faf28" + "sha256:8788e9155fe14f54164c1b9eb0a319d98ef02c160725587ad60f14ddc57b6f96", + "sha256:df277cb51e61359aba502208d680f90c0493adec6f0e848af94948778aed386e" ], - "version": "==0.33.6" + "version": "==0.34.2" }, "wrapt": { "hashes": [ @@ -659,10 +700,10 @@ }, "zipp": { "hashes": [ - "sha256:3718b1cbcd963c7d4c5511a8240812904164b7f381b647143a89d3b98f9bcd8e", - "sha256:f06903e9f1f43b12d371004b4ac7b06ab39a44adc747266928ae6debfa7b3335" + "sha256:5c56e330306215cd3553342cfafc73dda2c60792384117893f3a83f8a1209f50", + "sha256:d65287feb793213ffe11c0f31b81602be31448f38aeb8ffc2eb286c4f6f6657e" ], - "version": "==0.6.0" + "version": "==2.2.0" } } } diff --git a/teslajsonpy/connection.py b/teslajsonpy/connection.py index c2084efe..23bc5e72 100644 --- a/teslajsonpy/connection.py +++ b/teslajsonpy/connection.py @@ -141,14 +141,25 @@ async def __open( url, headers=headers, data=data ) data = await resp.json() - _LOGGER.debug(json.dumps(data)) + _LOGGER.debug("%s: %s", resp.status, json.dumps(data)) if resp.status > 299: if resp.status == 401: - if "error" in data and data["error"] == "invalid_token": + if data.get("error") == "invalid_token": raise TeslaException(resp.status, "invalid_token") elif resp.status == 408: - return False + raise TeslaException(resp.status, "vehicle_unavailable") raise TeslaException(resp.status) + if data.get("error"): + # known errors: + # 'vehicle unavailable: {:error=>"vehicle unavailable:"}', + # "upstream_timeout", "vehicle is curently in service" + _LOGGER.debug( + "Raising exception for : %s", + f'{data.get("error")}:{data.get("error_description")}', + ) + raise TeslaException( + f'{data.get("error")}:{data.get("error_description")}' + ) except aiohttp.ClientResponseError as exception_: raise TeslaException(exception_.status) return data diff --git a/teslajsonpy/controller.py b/teslajsonpy/controller.py index 5d43e3f6..6cefa10d 100644 --- a/teslajsonpy/controller.py +++ b/teslajsonpy/controller.py @@ -11,6 +11,8 @@ import time from typing import Optional, Text, Tuple +import backoff + from teslajsonpy.battery_sensor import Battery, Range from teslajsonpy.binary_sensor import ( ChargerConnectionSensor, @@ -245,8 +247,15 @@ def valid_result(result): if inst.car_online.get(inst._id_to_vin(car_id)) or is_wake_command: try: result = await func(*args, **kwargs) - except TeslaException: - pass + except TeslaException as ex: + _LOGGER.debug( + "Exception: %s\n%s(%s %s)", + ex.message, + func.__name__, # pylint: disable=no-member, + args, + kwargs, + ) + raise if valid_result(result) or is_wake_command: return result _LOGGER.debug( @@ -288,37 +297,34 @@ def valid_result(result): retries += 1 continue inst.car_online[inst._id_to_vin(car_id)] = False - raise RetryLimitError("Reached retry limit; aborting") + raise RetryLimitError("Reached retry limit; aborting wake up") break - # try function five more times - retries = 0 - result = None + inst.car_online[inst._id_to_vin(car_id)] = True + # retry function _LOGGER.debug( "Retrying %s(%s %s)", func.__name__, # pylint: disable=no-member, args, kwargs, ) - while not valid_result(result): - await asyncio.sleep(15 + sleep_delay ** (retries + 1)) - try: - result = await func(*args, **kwargs) - _LOGGER.debug( - "%s(%s %s):\n Retry Attempt(%s): %s", - func.__name__, # pylint: disable=no-member, - args, - kwargs, - retries, - "Success" if valid_result(result) else result, - ) - except TeslaException: - pass - finally: - retries += 1 - if retries >= 5: - raise RetryLimitError("Reached retry limit; aborting") - inst.car_online[inst._id_to_vin(car_id)] = True - return result + try: + result = await func(*args, **kwargs) + _LOGGER.debug( + "Retry after wake up succeeded: %s", + "True" if valid_result(result) else result, + ) + except TeslaException as ex: + _LOGGER.debug( + "Exception: %s\n%s(%s %s)", + ex.message, + func.__name__, # pylint: disable=no-member, + args, + kwargs, + ) + raise + if valid_result(result): + return result + raise TeslaException("could_not_wake_buses") return wrapped @@ -326,6 +332,7 @@ async def get_vehicles(self): """Get vehicles json from TeslaAPI.""" return (await self.__connection.get("vehicles"))["response"] + @backoff.on_exception(backoff.expo, TeslaException, max_time=120, logger=__name__) @wake_up async def post(self, car_id, command, data=None, wake_if_asleep=True): # pylint: disable=unused-argument @@ -357,6 +364,7 @@ async def post(self, car_id, command, data=None, wake_if_asleep=True): data = data or {} return await self.__connection.post(f"vehicles/{car_id}/{command}", data=data) + @backoff.on_exception(backoff.expo, TeslaException, max_time=60, logger=__name__) @wake_up async def get(self, car_id, command, wake_if_asleep=False): # pylint: disable=unused-argument diff --git a/teslajsonpy/exceptions.py b/teslajsonpy/exceptions.py index 8d87a54a..83065193 100644 --- a/teslajsonpy/exceptions.py +++ b/teslajsonpy/exceptions.py @@ -36,7 +36,7 @@ def __init__(self, code, *args, **kwargs): elif self.code == 503: self.message = "SERVICE_MAINTENANCE" elif self.code > 299: - self.message = "UNKNOWN_ERROR" + self.message = f"UNKNOWN_ERROR:{self.code}" class RetryLimitError(TeslaException): From a3728e59738778e1edf3b529c42467a7e7d4932c Mon Sep 17 00:00:00 2001 From: "Alan D. Tse" Date: Sat, 8 Feb 2020 23:13:36 -0800 Subject: [PATCH 20/40] refactor: black --- teslajsonpy/controller.py | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/teslajsonpy/controller.py b/teslajsonpy/controller.py index 6cefa10d..7e722f63 100644 --- a/teslajsonpy/controller.py +++ b/teslajsonpy/controller.py @@ -533,9 +533,7 @@ def _calculate_next_interval(vin: int) -> int: DRIVING_INTERVAL, ) return DRIVING_INTERVAL - if ( - cur_time - self.__last_parked_timestamp[vin] > IDLE_INTERVAL - ) and not ( + 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" @@ -546,15 +544,15 @@ def _calculate_next_interval(vin: int) -> int: "%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), + 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, + "%s scanning every %s seconds", vin[-5:], self.update_interval, ) return self.update_interval From 606c11ee0571e7cdc57b199e6409106a531340d1 Mon Sep 17 00:00:00 2001 From: "Alan D. Tse" Date: Sat, 8 Feb 2020 23:14:08 -0800 Subject: [PATCH 21/40] fix: prevent update for in_service cars --- teslajsonpy/controller.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/teslajsonpy/controller.py b/teslajsonpy/controller.py index 7e722f63..8f288a89 100644 --- a/teslajsonpy/controller.py +++ b/teslajsonpy/controller.py @@ -580,7 +580,7 @@ def _calculate_next_interval(vin: int) -> int: car_id = self._update_id(car_id) for vin, online in self.car_online.items(): # If specific car_id provided, only update match - if car_vin and car_vin != vin: + 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") From b2717e8906ec118baa62450ff2c7317b2d9d0028 Mon Sep 17 00:00:00 2001 From: "Alan D. Tse" Date: Sat, 8 Feb 2020 23:16:11 -0800 Subject: [PATCH 22/40] feat: add option to enable websockets --- teslajsonpy/controller.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/teslajsonpy/controller.py b/teslajsonpy/controller.py index 8f288a89..0a8d01a2 100644 --- a/teslajsonpy/controller.py +++ b/teslajsonpy/controller.py @@ -47,6 +47,7 @@ def __init__( access_token: Text = None, refresh_token: Text = None, update_interval: int = 300, + enable_websocket: bool = False, ) -> None: """Initialize controller. @@ -58,6 +59,7 @@ def __init__( refresh_token (Text, optional): Refresh token. Defaults to None. 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( @@ -87,6 +89,7 @@ def __init__( self.__websocket_listeners = [] self.__last_parked_timestamp = {} self.__update_state = {} + self.enable_websocket = enable_websocket async def connect(self, test_login=False) -> Tuple[Text, Text]: """Connect controller to Tesla.""" @@ -627,7 +630,8 @@ def _calculate_next_interval(vin: int) -> int: self._last_update_time[vin] = time.time() update_succeeded = True if ( - self.get_drive_params(car_id).get("shift_state") + self.enable_websocket + and self.get_drive_params(car_id).get("shift_state") and self.get_drive_params(car_id).get("shift_state") != "P" ): await self.__connection.websocket_connect( From 166013ca75bcb45962113f2a58982af54bacf4dd Mon Sep 17 00:00:00 2001 From: "Alan D. Tse" Date: Sat, 8 Feb 2020 23:27:53 -0800 Subject: [PATCH 23/40] fix: add vehicle_unavailable exception --- teslajsonpy/exceptions.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/teslajsonpy/exceptions.py b/teslajsonpy/exceptions.py index 83065193..7df7905d 100644 --- a/teslajsonpy/exceptions.py +++ b/teslajsonpy/exceptions.py @@ -27,6 +27,8 @@ def __init__(self, code, *args, **kwargs): self.message = "NOT_FOUND" elif self.code == 405: self.message = "MOBILE_ACCESS_DISABLED" + elif self.code == 408: + self.message = "VEHICLE_UNAVAILABLE" elif self.code == 423: self.message = "ACCOUNT_LOCKED" elif self.code == 429: From eddfa32440cec00e8097d010d720f2bd99be540e Mon Sep 17 00:00:00 2001 From: "Alan D. Tse" Date: Sun, 9 Feb 2020 10:16:32 -0800 Subject: [PATCH 24/40] refactor: add handling of upstream_timeout --- teslajsonpy/exceptions.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/teslajsonpy/exceptions.py b/teslajsonpy/exceptions.py index 7df7905d..e344d68f 100644 --- a/teslajsonpy/exceptions.py +++ b/teslajsonpy/exceptions.py @@ -37,8 +37,10 @@ def __init__(self, code, *args, **kwargs): self.message = "SERVER_ERROR" elif self.code == 503: self.message = "SERVICE_MAINTENANCE" + elif self.code == 504: + self.message = "UPSTREAM_TIMEOUT" elif self.code > 299: - self.message = f"UNKNOWN_ERROR:{self.code}" + self.message = f"UNKNOWN_ERROR_{self.code}" class RetryLimitError(TeslaException): From 95badc278846bbfede21a63846b77dd16a2fd05f Mon Sep 17 00:00:00 2001 From: "Alan D. Tse" Date: Sun, 9 Feb 2020 14:45:54 -0800 Subject: [PATCH 25/40] fix: handle malformed websocket data --- teslajsonpy/controller.py | 61 ++++++++++++++++++++------------------- 1 file changed, 32 insertions(+), 29 deletions(-) diff --git a/teslajsonpy/controller.py b/teslajsonpy/controller.py index 0a8d01a2..2c4a2de1 100644 --- a/teslajsonpy/controller.py +++ b/teslajsonpy/controller.py @@ -796,34 +796,37 @@ def _process_websocket_message(self, data): ("heading", int), ] values = data["value"].split(",") - 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"] - self.__charging[vin]["timestamp"] = update_json["timestamp"] - self.__state[vin]["timestamp"] = update_json["timestamp"] - self.__driving[vin]["speed"] = update_json["speed"] - 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.__driving[vin]["heading"] = update_json["est_heading"] - self.__driving[vin]["latitude"] = update_json["est_lat"] - self.__driving[vin]["longitude"] = update_json["est_lng"] - self.__driving[vin]["power"] = update_json["power"] - 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.__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 + 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"] + self.__charging[vin]["timestamp"] = update_json["timestamp"] + self.__state[vin]["timestamp"] = update_json["timestamp"] + self.__driving[vin]["speed"] = update_json["speed"] + 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.__driving[vin]["heading"] = update_json["est_heading"] + self.__driving[vin]["latitude"] = update_json["est_lat"] + self.__driving[vin]["longitude"] = update_json["est_lng"] + self.__driving[vin]["power"] = update_json["power"] + 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.__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: + _LOGGER.debug("Websocket for %s malformed: %s", vin[-5:], values) for func in self.__websocket_listeners: func(data) From 4d07e07723deb35e1ce9ca4ba4c8ab4052cdae79 Mon Sep 17 00:00:00 2001 From: "Alan D. Tse" Date: Sun, 9 Feb 2020 19:25:20 -0800 Subject: [PATCH 26/40] fix: force further delay in backoff --- teslajsonpy/controller.py | 27 +++++++++++++++++++++++++-- 1 file changed, 25 insertions(+), 2 deletions(-) diff --git a/teslajsonpy/controller.py b/teslajsonpy/controller.py index 2c4a2de1..456dee25 100644 --- a/teslajsonpy/controller.py +++ b/teslajsonpy/controller.py @@ -35,6 +35,29 @@ _LOGGER = logging.getLogger(__name__) +def min_expo(base=2, factor=1, max_value=None, min_value=0): + # pylint: disable=invalid-name + """Generate value for exponential decay. + + Args: + base: The mathematical base of the exponentiation operation + factor: Factor to multiply the exponentation by. + max_value: The maximum value to yield. Once the value in the + true exponential sequence exceeds this, the value + of max_value will forever after be yielded. + min_value: The minimum value to yield. This is a constant minimum. + + """ + n = 0 + while True: + a = min_value + factor * base ** n + if max_value is None or a < max_value: + yield a + n += 1 + else: + yield max_value + + class Controller: # pylint: disable=too-many-public-methods """Controller for connections to Tesla Motors API.""" @@ -335,7 +358,7 @@ async def get_vehicles(self): """Get vehicles json from TeslaAPI.""" return (await self.__connection.get("vehicles"))["response"] - @backoff.on_exception(backoff.expo, TeslaException, max_time=120, logger=__name__) + @backoff.on_exception(min_expo, TeslaException, max_time=120, logger=__name__, min_value=15) @wake_up async def post(self, car_id, command, data=None, wake_if_asleep=True): # pylint: disable=unused-argument @@ -367,7 +390,7 @@ async def post(self, car_id, command, data=None, wake_if_asleep=True): data = data or {} return await self.__connection.post(f"vehicles/{car_id}/{command}", data=data) - @backoff.on_exception(backoff.expo, TeslaException, max_time=60, logger=__name__) + @backoff.on_exception(min_expo, TeslaException, max_time=120, logger=__name__, min_value=15) @wake_up async def get(self, car_id, command, wake_if_asleep=False): # pylint: disable=unused-argument From c0907b24640d89930099eab909481aea2ecb6c4d Mon Sep 17 00:00:00 2001 From: "Alan D. Tse" Date: Mon, 10 Feb 2020 00:21:36 -0800 Subject: [PATCH 27/40] refactor: convert to wrapt --- Pipfile | 1 + Pipfile.lock | 28 ++-- setup.py | 2 +- teslajsonpy/controller.py | 302 ++++++++++++++++++-------------------- 4 files changed, 165 insertions(+), 168 deletions(-) diff --git a/Pipfile b/Pipfile index 79e1208f..fab00cfd 100644 --- a/Pipfile +++ b/Pipfile @@ -17,6 +17,7 @@ black = "*" [packages] aiohttp = "*" backoff = "*" +wrapt = "*" [pipenv] allow_prereleases = true diff --git a/Pipfile.lock b/Pipfile.lock index 28e37604..b11922a8 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "81182701cadd01dcf3eb5056bd5b37a42cdc2cfa46f7489e8df4d3650edca98c" + "sha256": "56fd0cfd6dd3899803ad48904a136010a6bdde1495c8b2cbee1e70f4d8ac06bc" }, "pipfile-spec": 6, "requires": {}, @@ -41,6 +41,13 @@ ], "version": "==19.3.0" }, + "backoff": { + "hashes": [ + "sha256:5e73e2cbe780e1915a204799dba0a01896f45f4385e636bcca7a0614d879d0cd", + "sha256:b8fba021fac74055ac05eb7c7bfce4723aedde6cd0a504e5326bcb0bdd6d19a4" + ], + "version": "==1.10.0" + }, "chardet": { "hashes": [ "sha256:84ab92ed1c4d4f16916e05906b6b75a6c0fb5db821cc65e70cbd64a3e2a5eaae", @@ -85,6 +92,12 @@ ], "version": "==3.7.4.1" }, + "wrapt": { + "hashes": [ + "sha256:565a021fd19419476b9362b05eeaa094178de64f8361e44468f9e9d7843901e1" + ], + "version": "==1.11.2" + }, "yarl": { "hashes": [ "sha256:0c2ab325d33f1b824734b3ef51d4d54a54e0e7a23d13b86974507602334c2cce", @@ -130,13 +143,6 @@ ], "version": "==19.3.0" }, - "backoff": { - "hashes": [ - "sha256:5e73e2cbe780e1915a204799dba0a01896f45f4385e636bcca7a0614d879d0cd", - "sha256:b8fba021fac74055ac05eb7c7bfce4723aedde6cd0a504e5326bcb0bdd6d19a4" - ], - "version": "==1.10.0" - }, "black": { "hashes": [ "sha256:1b30e59be925fafc1ee4565e5e08abef6b03fe455102883820fe5ee2e4734e0b", @@ -514,10 +520,10 @@ }, "python-semantic-release": { "hashes": [ - "sha256:038f8961b617906db72634ef0746515b5de17e09cd3a69a6b0a1c020e6b5edb3", - "sha256:e00fae90765a380c2d619fdf0ad9c940a4b8b769bec964845c03edb9ca73af7f" + "sha256:71a0dfb59ab159f5a356cf06719be75c4457816ea2d090c9b3ffb2ce15f30618", + "sha256:98f8a4b21831c0aeae4331a4805b9c33d4945c6f2842bffac93b25972e40c303" ], - "version": "==4.4.0" + "version": "==4.5.0" }, "readme-renderer": { "hashes": [ diff --git a/setup.py b/setup.py index c292539e..72282316 100644 --- a/setup.py +++ b/setup.py @@ -29,7 +29,7 @@ VERSION = None # What packages are required for this module to be executed? -REQUIRED = ["aiohttp"] +REQUIRED = ["aiohttp", "backoff", "wrapt"] # What packages are optional? EXTRAS = { diff --git a/teslajsonpy/controller.py b/teslajsonpy/controller.py index 456dee25..1f5e5414 100644 --- a/teslajsonpy/controller.py +++ b/teslajsonpy/controller.py @@ -6,12 +6,12 @@ https://github.com/zabuldon/teslajsonpy """ import asyncio -from functools import wraps import logging import time -from typing import Optional, Text, Tuple +from typing import Callable, Optional, Text, Tuple import backoff +import wrapt from teslajsonpy.battery_sensor import Battery, Range from teslajsonpy.binary_sensor import ( @@ -58,6 +58,150 @@ def min_expo(base=2, factor=1, max_value=None, min_value=0): yield max_value +@wrapt.decorator +async def wake_up(wrapped, instance, args, kwargs) -> Callable: + # pylint: disable=protected-access + """Wrap a API func so it will attempt to wake the vehicle if asleep. + + The command wrapped is run once if the car_id was last reported + online. If wrapped detects the car_id is offline, five attempts + will be made to wake the vehicle to reissue the command. + + Raises + RetryLimitError: The wake_up has exceeded the 5 attempts. + TeslaException: Tesla connection errors + + Returns + Callable: Wrapped function that will wake_up + + """ + + 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["response"].get("result") is True + or result["response"].get("reason") + != "could_not_wake_buses" + ) + ) + ) + ) + except TypeError as exception: + _LOGGER.error("Result: %s, %s", result, exception) + + retries = 0 + sleep_delay = 2 + instance = args[0] + car_id = args[1] + is_wake_command = len(args) >= 3 and args[2] == "wake_up" + result = None + if instance.car_online.get(instance._id_to_vin(car_id)) or is_wake_command: + try: + result = await wrapped(*args, **kwargs) + except TeslaException as ex: + _LOGGER.debug( + "Exception: %s\n%s(%s %s)", + ex.message, + wrapped.__name__, + args, + kwargs, + ) + raise + if valid_result(result) or is_wake_command: + return result + _LOGGER.debug( + "wake_up needed for %s -> %s \n" + "Info: args:%s, kwargs:%s, " + "VIN:%s, car_online:%s", + wrapped.__name__, + result, + args, + kwargs, + instance._id_to_vin(car_id)[-5:], + instance.car_online, + ) + instance.car_online[instance._id_to_vin(car_id)] = False + while ( + kwargs.get("wake_if_asleep") + and + # Check online state + ( + car_id is None + or ( + not instance._id_to_vin(car_id) + or not instance.car_online.get(instance._id_to_vin(car_id)) + ) + ) + ): + _LOGGER.debug("Attempting to wake up") + result = await instance._wake_up(car_id) + _LOGGER.debug( + "%s(%s): Wake Attempt(%s): %s", + wrapped.__name__, + instance._id_to_vin(car_id)[-5:], + retries, + result, + ) + if not result: + if retries < 5: + await asyncio.sleep(15 + sleep_delay ** (retries + 2)) + retries += 1 + continue + instance.car_online[instance._id_to_vin(car_id)] = False + raise RetryLimitError("Reached retry limit; aborting wake up") + break + instance.car_online[instance._id_to_vin(car_id)] = True + # retry function + _LOGGER.debug( + "Retrying %s(%s %s)", + wrapped.__name__, + args, + kwargs, + ) + try: + result = await wrapped(*args, **kwargs) + _LOGGER.debug( + "Retry after wake up succeeded: %s", + "True" if valid_result(result) else result, + ) + except TeslaException as ex: + _LOGGER.debug( + "Exception: %s\n%s(%s %s)", + ex.message, + wrapped.__name__, + args, + kwargs, + ) + raise + if valid_result(result): + return result + raise TeslaException("could_not_wake_buses") + + class Controller: # pylint: disable=too-many-public-methods """Controller for connections to Tesla Motors API.""" @@ -200,160 +344,6 @@ def register_websocket_callback(self, callback) -> int: self.__websocket_listeners.append(callback) return len(self.__websocket_listeners) - 1 - 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 func so it will attempt to wake the vehicle if asleep. - - The command func is run once if the car_id was last reported - online. Assuming func 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 - car_id (string): The vehicle to attempt to wake. - TODO: This currently requires a car_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 func - - Throws: - RetryLimitError - - """ - - @wraps(func) - async 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["response"].get("result") is True - or result["response"].get("reason") - != "could_not_wake_buses" - ) - ) - ) - ) - except TypeError as exception: - _LOGGER.error("Result: %s, %s", result, exception) - - retries = 0 - sleep_delay = 2 - inst = args[0] - car_id = args[1] - is_wake_command = len(args) >= 3 and args[2] == "wake_up" - result = None - if inst.car_online.get(inst._id_to_vin(car_id)) or is_wake_command: - try: - result = await func(*args, **kwargs) - except TeslaException as ex: - _LOGGER.debug( - "Exception: %s\n%s(%s %s)", - ex.message, - func.__name__, # pylint: disable=no-member, - args, - kwargs, - ) - raise - if valid_result(result) or is_wake_command: - return result - _LOGGER.debug( - "wake_up needed for %s -> %s \n" - "Info: args:%s, kwargs:%s, " - "VIN:%s, car_online:%s", - func.__name__, # pylint: disable=no-member - result, - args, - kwargs, - inst._id_to_vin(car_id)[-5:], - inst.car_online, - ) - inst.car_online[inst._id_to_vin(car_id)] = False - while ( - kwargs.get("wake_if_asleep") - and - # Check online state - ( - car_id is None - or ( - not inst._id_to_vin(car_id) - or not inst.car_online.get(inst._id_to_vin(car_id)) - ) - ) - ): - _LOGGER.debug("Attempting to wake up") - result = await inst._wake_up(car_id) - _LOGGER.debug( - "%s(%s): Wake Attempt(%s): %s", - func.__name__, # pylint: disable=no-member, - inst._id_to_vin(car_id)[-5:], - retries, - result, - ) - if not result: - if retries < 5: - await asyncio.sleep(15 + sleep_delay ** (retries + 2)) - retries += 1 - continue - inst.car_online[inst._id_to_vin(car_id)] = False - raise RetryLimitError("Reached retry limit; aborting wake up") - break - inst.car_online[inst._id_to_vin(car_id)] = True - # retry function - _LOGGER.debug( - "Retrying %s(%s %s)", - func.__name__, # pylint: disable=no-member, - args, - kwargs, - ) - try: - result = await func(*args, **kwargs) - _LOGGER.debug( - "Retry after wake up succeeded: %s", - "True" if valid_result(result) else result, - ) - except TeslaException as ex: - _LOGGER.debug( - "Exception: %s\n%s(%s %s)", - ex.message, - func.__name__, # pylint: disable=no-member, - args, - kwargs, - ) - raise - if valid_result(result): - return result - raise TeslaException("could_not_wake_buses") - - return wrapped - async def get_vehicles(self): """Get vehicles json from TeslaAPI.""" return (await self.__connection.get("vehicles"))["response"] From 383ce46cc89608054e83cbdf0d840d7e95b88727 Mon Sep 17 00:00:00 2001 From: "Alan D. Tse" Date: Mon, 10 Feb 2020 23:26:04 -0800 Subject: [PATCH 28/40] refactor: change backoff behavior to commands only --- teslajsonpy/controller.py | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/teslajsonpy/controller.py b/teslajsonpy/controller.py index 1f5e5414..5c530fdc 100644 --- a/teslajsonpy/controller.py +++ b/teslajsonpy/controller.py @@ -115,9 +115,8 @@ def valid_result(result): retries = 0 sleep_delay = 2 - instance = args[0] - car_id = args[1] - is_wake_command = len(args) >= 3 and args[2] == "wake_up" + car_id = args[0] + is_wake_command = len(args) >= 2 and args[1] == "wake_up" result = None if instance.car_online.get(instance._id_to_vin(car_id)) or is_wake_command: try: @@ -348,7 +347,6 @@ async def get_vehicles(self): """Get vehicles json from TeslaAPI.""" return (await self.__connection.get("vehicles"))["response"] - @backoff.on_exception(min_expo, TeslaException, max_time=120, logger=__name__, min_value=15) @wake_up async def post(self, car_id, command, data=None, wake_if_asleep=True): # pylint: disable=unused-argument @@ -380,7 +378,6 @@ async def post(self, car_id, command, data=None, wake_if_asleep=True): data = data or {} return await self.__connection.post(f"vehicles/{car_id}/{command}", data=data) - @backoff.on_exception(min_expo, TeslaException, max_time=120, logger=__name__, min_value=15) @wake_up async def get(self, car_id, command, wake_if_asleep=False): # pylint: disable=unused-argument @@ -439,6 +436,7 @@ async def data_request(self, car_id, name, wake_if_asleep=False): ) )["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. From f38d2e69b9145b1dcc945fa19215814205a4e0ff Mon Sep 17 00:00:00 2001 From: "Alan D. Tse" Date: Mon, 10 Feb 2020 23:27:03 -0800 Subject: [PATCH 29/40] docs: fix typos --- teslajsonpy/binary_sensor.py | 2 +- teslajsonpy/controller.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/teslajsonpy/binary_sensor.py b/teslajsonpy/binary_sensor.py index e34ed7eb..ef8c85e5 100644 --- a/teslajsonpy/binary_sensor.py +++ b/teslajsonpy/binary_sensor.py @@ -156,5 +156,5 @@ def has_battery() -> bool: return False def get_value(self) -> bool: - """Return the battery level.""" + """Return the car is online.""" return self.__online_state diff --git a/teslajsonpy/controller.py b/teslajsonpy/controller.py index 5c530fdc..3b3277e7 100644 --- a/teslajsonpy/controller.py +++ b/teslajsonpy/controller.py @@ -65,7 +65,7 @@ async def wake_up(wrapped, instance, args, kwargs) -> Callable: The command wrapped is run once if the car_id was last reported online. If wrapped detects the car_id is offline, five attempts - will be made to wake the vehicle to reissue the command. + will be made to wake the vehicle to retry the command. Raises RetryLimitError: The wake_up has exceeded the 5 attempts. From d381d53e446c481aabfc0384260d3e66e769b04c Mon Sep 17 00:00:00 2001 From: "Alan D. Tse" Date: Mon, 10 Feb 2020 23:45:36 -0800 Subject: [PATCH 30/40] refactor: change wake_if_asleep option for connect --- teslajsonpy/controller.py | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/teslajsonpy/controller.py b/teslajsonpy/controller.py index 3b3277e7..8d02b9fd 100644 --- a/teslajsonpy/controller.py +++ b/teslajsonpy/controller.py @@ -257,8 +257,18 @@ def __init__( self.__update_state = {} self.enable_websocket = enable_websocket - async def connect(self, test_login=False) -> Tuple[Text, Text]: - """Connect controller to Tesla.""" + async def connect(self, test_login=False, wake_if_asleep=False) -> Tuple[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. + + Returns + Tuple[Text, Text]: Returns the refresh_token and access_token + + """ + cars = await self.get_vehicles() if test_login: return (self.__connection.refresh_token, self.__connection.access_token) @@ -302,7 +312,7 @@ async def connect(self, test_login=False) -> Tuple[Text, Text]: self.__components.append(Odometer(car, self)) self.__components.append(OnlineSensor(car, self)) - tasks = [self.update(car["id"], wake_if_asleep=True) for car in cars] + tasks = [self.update(car["id"], wake_if_asleep=wake_if_asleep) for car in cars] try: await asyncio.gather(*tasks) except (TeslaException, RetryLimitError): From 690af0e9fae925d388d2d8fb508a2c784e4c7b68 Mon Sep 17 00:00:00 2001 From: "Alan D. Tse" Date: Thu, 13 Feb 2020 22:30:01 -0800 Subject: [PATCH 31/40] docs: add additional websocket logging --- teslajsonpy/connection.py | 3 ++- teslajsonpy/controller.py | 6 ++++++ 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/teslajsonpy/connection.py b/teslajsonpy/connection.py index 23bc5e72..6c1ba464 100644 --- a/teslajsonpy/connection.py +++ b/teslajsonpy/connection.py @@ -201,7 +201,7 @@ async def _process_messages() -> None: and msg_json["value"] == "disconnected" ): if kwargs.get("on_disconnect"): - kwargs.get("on_disconnect")() + kwargs.get("on_disconnect")(msg_json) if kwargs.get("on_message"): kwargs.get("on_message")(msg_json) elif msg.type == aiohttp.WSMsgType.ERROR: @@ -213,6 +213,7 @@ async def _process_messages() -> None: self.websocket = await self.websession.ws_connect(self.websocket_url) loop = asyncio.get_event_loop() loop.create_task(_process_messages()) + _LOGGER.debug("%s:Trying to subscribe to websocket", vin[-5:]) await self.websocket.send_json( data={ "msg_type": "data:subscribe_oauth", diff --git a/teslajsonpy/controller.py b/teslajsonpy/controller.py index 8d02b9fd..97be77be 100644 --- a/teslajsonpy/controller.py +++ b/teslajsonpy/controller.py @@ -659,6 +659,7 @@ def _calculate_next_interval(vin: int) -> int: vin[-5:], self.__vin_vehicle_id_map[vin], on_message=self._process_websocket_message, + on_disconnect=self._process_websocket_disconnect, ) return update_succeeded @@ -851,3 +852,8 @@ def _process_websocket_message(self, data): _LOGGER.debug("Websocket for %s malformed: %s", vin[-5:], values) 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:]) From d3afe61fea7c216a509fd630dea85dff0728a081 Mon Sep 17 00:00:00 2001 From: "Alan D. Tse" Date: Sat, 15 Feb 2020 01:01:32 -0800 Subject: [PATCH 32/40] fix: update websocket subscribe and retry This is the subscribe command from the latest app --- teslajsonpy/connection.py | 112 +++++++++++++++++++++++++++++++++++--- teslajsonpy/const.py | 1 + teslajsonpy/controller.py | 70 +++++++++++++++--------- 3 files changed, 149 insertions(+), 34 deletions(-) diff --git a/teslajsonpy/connection.py b/teslajsonpy/connection.py index 6c1ba464..e6d1aff3 100644 --- a/teslajsonpy/connection.py +++ b/teslajsonpy/connection.py @@ -10,12 +10,14 @@ import datetime import json import logging +import time from typing import Dict, Text import aiohttp from yarl import URL from teslajsonpy.exceptions import IncompleteCredentials, TeslaException +from teslajsonpy.const import DRIVING_INTERVAL, WEBSOCKET_TIMEOUT _LOGGER = logging.getLogger(__name__) @@ -179,6 +181,8 @@ async def websocket_connect(self, vin: int, vehicle_id: int, **kwargs): async def _process_messages() -> None: """Start Async WebSocket Listener.""" + nonlocal last_message_time + nonlocal disconnected async for msg in self.websocket: _LOGGER.debug("msg: %s", msg) if msg.type == aiohttp.WSMsgType.BINARY: @@ -189,6 +193,8 @@ async def _process_messages() -> None: self.websocket_url, vin[-5:], ) + if msg_json["msg_type"] == "data:update": + last_message_time = time.time() if ( msg_json["msg_type"] == "data:error" and msg_json["value"] == "Can't validate token. " @@ -202,23 +208,111 @@ async def _process_messages() -> None: ): if kwargs.get("on_disconnect"): kwargs.get("on_disconnect")(msg_json) + disconnected = True if kwargs.get("on_message"): kwargs.get("on_message")(msg_json) elif msg.type == aiohttp.WSMsgType.ERROR: _LOGGER.debug("WSMsgType error") break + disconnected = False + last_message_time = time.time() + timeout = last_message_time + DRIVING_INTERVAL if not self.websocket or self.websocket.closed: _LOGGER.debug("Connecting to websocket %s", self.websocket_url) self.websocket = await self.websession.ws_connect(self.websocket_url) loop = asyncio.get_event_loop() loop.create_task(_process_messages()) - _LOGGER.debug("%s:Trying to subscribe to websocket", vin[-5:]) - await self.websocket.send_json( - data={ - "msg_type": "data:subscribe_oauth", - "token": self.access_token, - "value": "speed,odometer,soc,elevation,est_heading,est_lat,est_lng,power,shift_state,range,est_range,heading", - "tag": f"{vehicle_id}", - } - ) + while not ( + disconnected + or time.time() - last_message_time > WEBSOCKET_TIMEOUT + or time.time() > timeout + ): + _LOGGER.debug("%s:Trying to subscribe to websocket", vin[-5:]) + await self.websocket.send_json( + data={ + "msg_type": "data:subscribe_oauth", + "token": self.access_token, + "value": "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", + # "value": "speed,odometer,soc,elevation,est_heading,est_lat,est_lng,power,shift_state,range,est_range,heading", + # old values + "tag": f"{vehicle_id}", + "created:timestamp": round(time.time() * 1000), + } + ) + await asyncio.sleep(WEBSOCKET_TIMEOUT - 1) + + # async def websocket_connect2(self, vin: int, vehicle_id: int, **kwargs): + # """Connect to Tesla streaming websocket. + + # Args: + # vin (int): vin of vehicle + # vehicle_id (int): vehicle_id from Tesla api + # on_message (function): function to call on a valid message. It must + # process a json delivered in data + # on_disconnect (function): function to call on a disconnect message. It must + # process a json delivered in data + + # """ + + # async def _process_messages() -> None: + # """Start Async WebSocket Listener.""" + # async for msg in self.websocket[vin]["websocket"]: + # _LOGGER.debug("%s:msg: %s", vin[-5:], msg) + # if msg.type == aiohttp.WSMsgType.BINARY: + # msg_json = json.loads(msg.data) + # if msg_json["msg_type"] == "control:hello": + # _LOGGER.debug( + # "%s:Succesfully connected to websocket %s on %s", + # vin[-5:], + # self.websocket_url, + # task, + # ) + # if ( + # msg_json["msg_type"] == "data:error" + # and msg_json["value"] == "Can't validate token. " + # ): + # raise TeslaException( + # "Can't validate token for websocket connection." + # ) + # if ( + # msg_json["msg_type"] == "data:error" + # and msg_json["value"] == "disconnected" + # ): + # if self.websocket[vin].kwargs.get("on_disconnect"): + # self.websocket[vin].kwargs.get("on_disconnect")() + # self.websocket[vin].pop(None) + # _LOGGER.debug( + # "%s:Disconnecting from websocket on %s", vin[-5:], task + # ) + # await self.websocket[vin]["websocket"].close() + # if kwargs.get("on_message"): + # kwargs.get("on_message")(msg_json) + # elif msg.type == aiohttp.WSMsgType.ERROR: + # _LOGGER.debug("WSMsgType error") + # break + + # self.websocket.setdefault(vin, {"websocket": None, "kwargs": kwargs}) + # if ( + # not self.websocket[vin]["websocket"] + # or self.websocket[vin]["websocket"].closed + # ): + # _LOGGER.debug("%s:Connecting to websocket %s", vin[-5:], self.websocket_url) + # self.websocket[vin]["websocket"] = await self.websession.ws_connect( + # self.websocket_url + # ) + # loop = asyncio.get_event_loop() + # task = loop.create_task(_process_messages()) + # _LOGGER.debug( + # "%s:Trying to subscribe to websocket: %s", vin[-5:], self.access_token + # ) + + # await self.websocket[vin]["websocket"].send_json( + # data={ + # "msg_type": "data:subscribe_oauth", + # # "token": "self.access_token", + # "token": self.access_token, + # "value": "speed,odometer,soc,elevation,est_heading,est_lat,est_lng,power,shift_state,range,est_range,heading", + # "tag": f"{vehicle_id}", + # } + # ) diff --git a/teslajsonpy/const.py b/teslajsonpy/const.py index ffd4dd9f..a755d201 100644 --- a/teslajsonpy/const.py +++ b/teslajsonpy/const.py @@ -9,3 +9,4 @@ ONLINE_INTERVAL = 60 # interval for checking online state; does not hit individual cars SLEEP_INTERVAL = 660 # interval required to let vehicle sleep; based on testing DRIVING_INTERVAL = 60 # interval when driving detected +WEBSOCKET_TIMEOUT = 11 # time for websocket to timeout diff --git a/teslajsonpy/controller.py b/teslajsonpy/controller.py index 97be77be..6b104a6e 100644 --- a/teslajsonpy/controller.py +++ b/teslajsonpy/controller.py @@ -802,20 +802,28 @@ def _process_websocket_message(self, data): 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), - ("odometer", float), - ("soc", int), - ("elevation", int), - ("est_heading", int), + ("power", int), ("est_lat", float), ("est_lng", float), - ("power", int), - ("shift_state", str), - ("range", int), - ("est_range", int), - ("heading", int), + ("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: @@ -823,20 +831,10 @@ def _process_websocket_message(self, data): 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"] - self.__charging[vin]["timestamp"] = update_json["timestamp"] - self.__state[vin]["timestamp"] = update_json["timestamp"] - self.__driving[vin]["speed"] = update_json["speed"] - 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.__driving[vin]["heading"] = update_json["est_heading"] - self.__driving[vin]["latitude"] = update_json["est_lat"] - self.__driving[vin]["longitude"] = update_json["est_lng"] - self.__driving[vin]["power"] = update_json["power"] if ( self.__driving[vin].get("shift_state") - and self.__driving[vin].get("shift_state") != update_json["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" @@ -844,12 +842,34 @@ def _process_websocket_message(self, data): ): self.__last_parked_timestamp[vin] = update_json["timestamp"] / 1000 self.__driving[vin]["shift_state"] = update_json["shift_state"] - self.__charging[vin]["battery_range"] = update_json["range"] - self.__charging[vin]["est_battery_range"] = update_json["est_range"] + 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_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: - _LOGGER.debug("Websocket for %s malformed: %s", vin[-5:], values) + 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) From 4ec8d9b24a7d1d020372ae20ea5bf86d748a3f01 Mon Sep 17 00:00:00 2001 From: "Alan D. Tse" Date: Sat, 15 Feb 2020 22:34:04 -0800 Subject: [PATCH 33/40] refactor: change default value to None --- teslajsonpy/battery_sensor.py | 8 ++++---- teslajsonpy/binary_sensor.py | 4 ++-- teslajsonpy/charger.py | 16 ++++++++-------- teslajsonpy/climate.py | 18 +++++++++--------- teslajsonpy/gps.py | 26 +++++++++++++------------- teslajsonpy/lock.py | 2 +- 6 files changed, 37 insertions(+), 37 deletions(-) diff --git a/teslajsonpy/battery_sensor.py b/teslajsonpy/battery_sensor.py index feb3cfdc..347e81e1 100644 --- a/teslajsonpy/battery_sensor.py +++ b/teslajsonpy/battery_sensor.py @@ -23,7 +23,7 @@ def __init__(self, data: Dict, controller) -> None: """ super().__init__(data, controller) - self.__battery_level: int = 0 + self.__battery_level: int = None self.__charging_state: bool = None self.__charge_port_door_open: bool = None self.type: Text = "battery sensor" @@ -85,9 +85,9 @@ def __init__(self, data: Dict, controller) -> None: """ super().__init__(data, controller) - self.__battery_range = 0 - self.__est_battery_range = 0 - self.__ideal_battery_range = 0 + self.__battery_range = None + self.__est_battery_range = None + self.__ideal_battery_range = None self.type = "range sensor" self.__rated = True self.measurement = "LENGTH_MILES" diff --git a/teslajsonpy/binary_sensor.py b/teslajsonpy/binary_sensor.py index ef8c85e5..b72a3723 100644 --- a/teslajsonpy/binary_sensor.py +++ b/teslajsonpy/binary_sensor.py @@ -33,7 +33,7 @@ def __init__(self, data: Dict, controller): """ super().__init__(data, controller) - self.__state = False + self.__state = None self.type = "parking brake sensor" self.hass_type = "binary_sensor" @@ -90,7 +90,7 @@ def __init__(self, data, controller): """ super().__init__(data, controller) - self.__state = False + self.__state = None self.type = "charger sensor" self.hass_type = "binary_sensor" diff --git a/teslajsonpy/charger.py b/teslajsonpy/charger.py index e8dad333..a684f774 100644 --- a/teslajsonpy/charger.py +++ b/teslajsonpy/charger.py @@ -32,7 +32,7 @@ def __init__(self, data, controller): """ super().__init__(data, controller) self.__manual_update_time = 0 - self.__charger_state = False + self.__charger_state = None self.type = "charger switch" self.hass_type = "switch" self.name = self._name() @@ -87,7 +87,7 @@ def __init__(self, data, controller): """Initialize the charger range switch.""" super().__init__(data, controller) self.__manual_update_time = 0 - self.__maxrange_state = False + self.__maxrange_state = None self.type = "maxrange switch" self.hass_type = "switch" self.name = self._name() @@ -154,12 +154,12 @@ def __init__(self, data: Dict, controller) -> None: self.name: Text = self._name() self.uniq_name: Text = self._uniq_name() self.bin_type: hex = 0xC - self.__added_range = 0 - self.__charging_rate = 0 - self.__time_to_full = 0 - self.__charge_current_request = 0 - self.__charger_actual_current = 0 - self.__charger_voltage = 0 + self.__added_range = None + self.__charging_rate = None + self.__time_to_full = None + self.__charge_current_request = None + self.__charger_actual_current = None + self.__charger_voltage = None async def async_update(self) -> None: """Update the battery state.""" diff --git a/teslajsonpy/climate.py b/teslajsonpy/climate.py index 91abe038..3cc718f3 100644 --- a/teslajsonpy/climate.py +++ b/teslajsonpy/climate.py @@ -36,13 +36,13 @@ def __init__(self, data, controller): """ super().__init__(data, controller) - self.__is_auto_conditioning_on = False - self.__inside_temp = 0 - self.__outside_temp = 0 - self.__driver_temp_setting = 0 - self.__passenger_temp_setting = 0 - self.__is_climate_on = False - self.__fan_status = 0 + self.__is_auto_conditioning_on = None + self.__inside_temp = None + self.__outside_temp = None + self.__driver_temp_setting = None + self.__passenger_temp_setting = None + self.__is_climate_on = None + self.__fan_status = None self.__manual_update_time = 0 self.type = "HVAC (climate) system" @@ -161,8 +161,8 @@ def __init__(self, data, controller): """ super().__init__(data, controller) - self.__inside_temp = 0 - self.__outside_temp = 0 + self.__inside_temp = None + self.__outside_temp = None self.type = "temperature sensor" self.measurement = "C" diff --git a/teslajsonpy/gps.py b/teslajsonpy/gps.py index bfb7d2cc..a1718304 100644 --- a/teslajsonpy/gps.py +++ b/teslajsonpy/gps.py @@ -30,10 +30,10 @@ def __init__(self, data, controller): """ super().__init__(data, controller) - self.__longitude = 0 - self.__latitude = 0 - self.__heading = 0 - self.__speed = 0 + self.__longitude = None + self.__latitude = None + self.__heading = None + self.__speed = None self.__location = {} self.last_seen = 0 @@ -48,6 +48,13 @@ def __init__(self, data, controller): def get_location(self): """Return the current location.""" + if self.__longitude and self.__latitude and self.__heading: + self.__location = { + "longitude": self.__longitude, + "latitude": self.__latitude, + "heading": self.__heading, + "speed": self.__speed, + } return self.__location async def async_update(self): @@ -59,13 +66,6 @@ async def async_update(self): self.__latitude = data["latitude"] self.__heading = data["heading"] self.__speed = data["speed"] if data["speed"] else 0 - if self.__longitude and self.__latitude and self.__heading: - self.__location = { - "longitude": self.__longitude, - "latitude": self.__latitude, - "heading": self.__heading, - "speed": self.__speed, - } @staticmethod def has_battery(): @@ -93,7 +93,7 @@ def __init__(self, data, controller): """ super().__init__(data, controller) - self.__odometer = 0 + self.__odometer = None self.type = "mileage sensor" self.measurement = "LENGTH_MILES" self.hass_type = "sensor" @@ -124,7 +124,7 @@ def has_battery(): def get_value(self): """Return the odometer reading.""" - return round(self.__odometer, 1) + return round(self.__odometer, 1) if self.__odometer else None @property def device_class(self) -> Text: diff --git a/teslajsonpy/lock.py b/teslajsonpy/lock.py index 4421cb99..268594a2 100644 --- a/teslajsonpy/lock.py +++ b/teslajsonpy/lock.py @@ -106,7 +106,7 @@ def __init__(self, data, controller): """ super().__init__(data, controller) self.__manual_update_time = 0 - self.__lock_state = False + self.__lock_state = None self.type = "charger door lock" self.hass_type = "lock" From 500968a04b5773e4c2e63a49192dd4fc5c935a7a Mon Sep 17 00:00:00 2001 From: "Alan D. Tse" Date: Sat, 15 Feb 2020 22:35:41 -0800 Subject: [PATCH 34/40] feat: add wake_if_asleep param to update --- teslajsonpy/battery_sensor.py | 8 ++++---- teslajsonpy/binary_sensor.py | 12 ++++++------ teslajsonpy/charger.py | 12 ++++++------ teslajsonpy/climate.py | 8 ++++---- teslajsonpy/gps.py | 8 ++++---- teslajsonpy/lock.py | 8 ++++---- teslajsonpy/vehicle.py | 4 ++-- 7 files changed, 30 insertions(+), 30 deletions(-) diff --git a/teslajsonpy/battery_sensor.py b/teslajsonpy/battery_sensor.py index 347e81e1..b86a2c75 100644 --- a/teslajsonpy/battery_sensor.py +++ b/teslajsonpy/battery_sensor.py @@ -34,9 +34,9 @@ def __init__(self, data: Dict, controller) -> None: self.uniq_name: Text = self._uniq_name() self.bin_type: hex = 0x5 - async def async_update(self) -> None: + async def async_update(self, wake_if_asleep=False) -> None: """Update the battery state.""" - await super().async_update() + await super().async_update(wake_if_asleep=wake_if_asleep) data = self._controller.get_charging_params(self._id) if data: self.__battery_level = data["battery_level"] @@ -97,9 +97,9 @@ def __init__(self, data: Dict, controller) -> None: self.uniq_name = self._uniq_name() self.bin_type = 0xA - async def async_update(self): + async def async_update(self, wake_if_asleep=False) -> None: """Update the battery range state.""" - await super().async_update() + await super().async_update(wake_if_asleep=wake_if_asleep) data = self._controller.get_charging_params(self._id) if data: self.__battery_range = data["battery_range"] diff --git a/teslajsonpy/binary_sensor.py b/teslajsonpy/binary_sensor.py index b72a3723..acd06ab2 100644 --- a/teslajsonpy/binary_sensor.py +++ b/teslajsonpy/binary_sensor.py @@ -44,9 +44,9 @@ def __init__(self, data: Dict, controller): self.uniq_name = self._uniq_name() self.bin_type = 0x1 - async def async_update(self): + async def async_update(self, wake_if_asleep=False) -> None: """Update the parking brake sensor.""" - await super().async_update() + await super().async_update(wake_if_asleep=wake_if_asleep) data = self._controller.get_drive_params(self._id) if data: self.attrs["shift_state"] = ( @@ -100,9 +100,9 @@ def __init__(self, data, controller): self.uniq_name = self._uniq_name() self.bin_type = 0x2 - async def async_update(self): + async def async_update(self, wake_if_asleep=False) -> None: """Update the charger connection sensor.""" - await super().async_update() + await super().async_update(wake_if_asleep=wake_if_asleep) data = self._controller.get_charging_params(self._id) if data: self.attrs["charging_state"] = data["charging_state"] @@ -144,9 +144,9 @@ def __init__(self, data: Dict, controller) -> None: self.name: Text = self._name() self.uniq_name: Text = self._uniq_name() - async def async_update(self) -> None: + async def async_update(self, wake_if_asleep=False) -> None: """Update the battery state.""" - await super().async_update() + await super().async_update(wake_if_asleep=wake_if_asleep) self.__online_state = self._controller.car_online[self._vin] self.attrs["state"] = self._controller.car_state[self._vin].get("state") diff --git a/teslajsonpy/charger.py b/teslajsonpy/charger.py index a684f774..c890cc1c 100644 --- a/teslajsonpy/charger.py +++ b/teslajsonpy/charger.py @@ -39,9 +39,9 @@ def __init__(self, data, controller): self.uniq_name = self._uniq_name() self.bin_type = 0x8 - async def async_update(self): + async def async_update(self, wake_if_asleep=False) -> None: """Update the charging state of the Tesla Vehicle.""" - await super().async_update() + await super().async_update(wake_if_asleep=wake_if_asleep) last_update = self._controller.get_last_update_time(self._id) if last_update >= self.__manual_update_time: data = self._controller.get_charging_params(self._id) @@ -94,9 +94,9 @@ def __init__(self, data, controller): self.uniq_name = self._uniq_name() self.bin_type = 0x9 - async def async_update(self): + async def async_update(self, wake_if_asleep=False) -> None: """Update the status of the range setting.""" - await super().async_update() + await super().async_update(wake_if_asleep=wake_if_asleep) last_update = self._controller.get_last_update_time(self._id) if last_update >= self.__manual_update_time: data = self._controller.get_charging_params(self._id) @@ -161,9 +161,9 @@ def __init__(self, data: Dict, controller) -> None: self.__charger_actual_current = None self.__charger_voltage = None - async def async_update(self) -> None: + async def async_update(self, wake_if_asleep=False) -> None: """Update the battery state.""" - await super().async_update() + await super().async_update(wake_if_asleep=wake_if_asleep) data = self._controller.get_gui_params(self._id) if data: self.measurement = data["gui_distance_units"] diff --git a/teslajsonpy/climate.py b/teslajsonpy/climate.py index 3cc718f3..d5975ad1 100644 --- a/teslajsonpy/climate.py +++ b/teslajsonpy/climate.py @@ -70,9 +70,9 @@ def get_fan_status(self): """Return fan status.""" return self.__fan_status - async def async_update(self): + async def async_update(self, wake_if_asleep=False) -> None: """Update the HVAC state.""" - await super().async_update() + await super().async_update(wake_if_asleep=wake_if_asleep) data = self._controller.get_climate_params(self._id) if data: last_update = self._controller.get_last_update_time(self._id) @@ -180,9 +180,9 @@ def get_outside_temp(self): """Get outside temperature.""" return self.__outside_temp - async def async_update(self): + async def async_update(self, wake_if_asleep=False) -> None: """Update the temperature.""" - await super().async_update() + await super().async_update(wake_if_asleep=wake_if_asleep) data = self._controller.get_climate_params(self._id) if data: self.__inside_temp = ( diff --git a/teslajsonpy/gps.py b/teslajsonpy/gps.py index a1718304..3a76ce9c 100644 --- a/teslajsonpy/gps.py +++ b/teslajsonpy/gps.py @@ -57,9 +57,9 @@ def get_location(self): } return self.__location - async def async_update(self): + async def async_update(self, wake_if_asleep=False) -> None: """Update the current GPS location.""" - await super().async_update() + await super().async_update(wake_if_asleep=wake_if_asleep) data = self._controller.get_drive_params(self._id) if data: self.__longitude = data["longitude"] @@ -103,9 +103,9 @@ def __init__(self, data, controller): self.bin_type = 0xB self.__rated = True - async def async_update(self): + async def async_update(self, wake_if_asleep=False) -> None: """Update the odometer and the unit of measurement based on GUI.""" - await super().async_update() + await super().async_update(wake_if_asleep=wake_if_asleep) 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 268594a2..e32a4387 100644 --- a/teslajsonpy/lock.py +++ b/teslajsonpy/lock.py @@ -44,9 +44,9 @@ def __init__(self, data, controller): self.uniq_name = self._uniq_name() self.bin_type = 0x7 - async def async_update(self): + async def async_update(self, wake_if_asleep=False) -> None: """Update the lock state.""" - await super().async_update() + await super().async_update(wake_if_asleep=wake_if_asleep) last_update = self._controller.get_last_update_time(self._id) if last_update >= self.__manual_update_time: data = self._controller.get_state_params(self._id) @@ -116,9 +116,9 @@ def __init__(self, data, controller): self.uniq_name = self._uniq_name() self.bin_type = 0x7 - async def async_update(self): + async def async_update(self, wake_if_asleep=False) -> None: """Update state of the charger lock.""" - await super().async_update() + await super().async_update(wake_if_asleep=wake_if_asleep) last_update = self._controller.get_last_update_time(self._id) if last_update >= self.__manual_update_time: data = self._controller.get_charging_params(self._id) diff --git a/teslajsonpy/vehicle.py b/teslajsonpy/vehicle.py index eb3a0f51..88f6ac1e 100644 --- a/teslajsonpy/vehicle.py +++ b/teslajsonpy/vehicle.py @@ -93,9 +93,9 @@ def assumed_state(self): > self._controller.update_interval ) - async def async_update(self): + async def async_update(self, wake_if_asleep=False): """Update the car version.""" - await self._controller.update(self.id(), wake_if_asleep=False) + await self._controller.update(self.id(), wake_if_asleep=wake_if_asleep) state = self._controller.get_state_params(self.id()) if state and "car_version" in state: self._car_version = state["car_version"] From 468df5a24e97ab0c1901b334b6a9772fabf86f42 Mon Sep 17 00:00:00 2001 From: "Alan D. Tse" Date: Sat, 15 Feb 2020 22:36:13 -0800 Subject: [PATCH 35/40] style: black --- teslajsonpy/controller.py | 56 +++++++++++++++++---------------------- 1 file changed, 25 insertions(+), 31 deletions(-) diff --git a/teslajsonpy/controller.py b/teslajsonpy/controller.py index 6b104a6e..6bddd055 100644 --- a/teslajsonpy/controller.py +++ b/teslajsonpy/controller.py @@ -123,11 +123,7 @@ def valid_result(result): result = await wrapped(*args, **kwargs) except TeslaException as ex: _LOGGER.debug( - "Exception: %s\n%s(%s %s)", - ex.message, - wrapped.__name__, - args, - kwargs, + "Exception: %s\n%s(%s %s)", ex.message, wrapped.__name__, args, kwargs ) raise if valid_result(result) or is_wake_command: @@ -175,12 +171,7 @@ def valid_result(result): break instance.car_online[instance._id_to_vin(car_id)] = True # retry function - _LOGGER.debug( - "Retrying %s(%s %s)", - wrapped.__name__, - args, - kwargs, - ) + _LOGGER.debug("Retrying %s(%s %s)", wrapped.__name__, args, kwargs) try: result = await wrapped(*args, **kwargs) _LOGGER.debug( @@ -189,11 +180,7 @@ def valid_result(result): ) except TeslaException as ex: _LOGGER.debug( - "Exception: %s\n%s(%s %s)", - ex.message, - wrapped.__name__, - args, - kwargs, + "Exception: %s\n%s(%s %s)", ex.message, wrapped.__name__, args, kwargs ) raise if valid_result(result): @@ -257,7 +244,9 @@ def __init__( self.__update_state = {} self.enable_websocket = enable_websocket - async def connect(self, test_login=False, wake_if_asleep=False) -> Tuple[Text, Text]: + async def connect( + self, test_login=False, wake_if_asleep=False + ) -> Tuple[Text, Text]: """Connect controller to Tesla. Args @@ -270,8 +259,6 @@ async def connect(self, test_login=False, wake_if_asleep=False) -> Tuple[Text, T """ cars = await self.get_vehicles() - if test_login: - return (self.__connection.refresh_token, self.__connection.access_token) self._last_attempted_update_time = time.time() self.__controller_lock = asyncio.Lock() @@ -312,11 +299,14 @@ async def connect(self, test_login=False, wake_if_asleep=False) -> Tuple[Text, T self.__components.append(Odometer(car, self)) self.__components.append(OnlineSensor(car, self)) - tasks = [self.update(car["id"], wake_if_asleep=wake_if_asleep) for car in cars] - try: - await asyncio.gather(*tasks) - except (TeslaException, RetryLimitError): - pass + if not test_login: + tasks = [ + self.update(car["id"], wake_if_asleep=wake_if_asleep) for car in cars + ] + try: + await asyncio.gather(*tasks) + except (TeslaException, RetryLimitError): + pass return (self.__connection.refresh_token, self.__connection.access_token) def is_token_refreshed(self) -> bool: @@ -446,7 +436,9 @@ async def data_request(self, car_id, name, wake_if_asleep=False): ) )["response"] - @backoff.on_exception(min_expo, TeslaException, max_time=60, logger=__name__, min_value=15) + @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. @@ -576,7 +568,7 @@ def _calculate_next_interval(vin: int) -> int: if self.__update_state[vin] != "normal": self.__update_state[vin] = "normal" _LOGGER.debug( - "%s scanning every %s seconds", vin[-5:], self.update_interval, + "%s scanning every %s seconds", vin[-5:], self.update_interval ) return self.update_interval @@ -655,11 +647,13 @@ def _calculate_next_interval(vin: int) -> int: and self.get_drive_params(car_id).get("shift_state") and self.get_drive_params(car_id).get("shift_state") != "P" ): - await self.__connection.websocket_connect( - vin[-5:], - self.__vin_vehicle_id_map[vin], - on_message=self._process_websocket_message, - on_disconnect=self._process_websocket_disconnect, + 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, + ) ) return update_succeeded From 3342d67a52a4c0ef88ef22858f58a5e47ee07384 Mon Sep 17 00:00:00 2001 From: "Alan D. Tse" Date: Sat, 15 Feb 2020 22:36:44 -0800 Subject: [PATCH 36/40] docs: streamline debug messages --- teslajsonpy/connection.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/teslajsonpy/connection.py b/teslajsonpy/connection.py index e6d1aff3..5cf9407c 100644 --- a/teslajsonpy/connection.py +++ b/teslajsonpy/connection.py @@ -189,9 +189,9 @@ async def _process_messages() -> None: msg_json = json.loads(msg.data) if msg_json["msg_type"] == "control:hello": _LOGGER.debug( - "Succesfully connected to websocket %s for %s", - self.websocket_url, + "%s:Succesfully connected to websocket %s", vin[-5:], + self.websocket_url, ) if msg_json["msg_type"] == "data:update": last_message_time = time.time() @@ -219,7 +219,7 @@ async def _process_messages() -> None: last_message_time = time.time() timeout = last_message_time + DRIVING_INTERVAL if not self.websocket or self.websocket.closed: - _LOGGER.debug("Connecting to websocket %s", self.websocket_url) + _LOGGER.debug("%s:Connecting to websocket %s", vin[-5:], self.websocket_url) self.websocket = await self.websession.ws_connect(self.websocket_url) loop = asyncio.get_event_loop() loop.create_task(_process_messages()) @@ -241,6 +241,9 @@ async def _process_messages() -> None: } ) await asyncio.sleep(WEBSOCKET_TIMEOUT - 1) + _LOGGER.debug( + "%s:Exiting websocket_connect", vin[-5:], + ) # async def websocket_connect2(self, vin: int, vehicle_id: int, **kwargs): # """Connect to Tesla streaming websocket. From 21c919eff33cedc5ca8148e51300c5c5f69055cc Mon Sep 17 00:00:00 2001 From: "Alan D. Tse" Date: Thu, 27 Feb 2020 00:20:43 -0800 Subject: [PATCH 37/40] fix: enable native_type gps --- teslajsonpy/controller.py | 1 + teslajsonpy/gps.py | 15 ++++++++++++--- 2 files changed, 13 insertions(+), 3 deletions(-) diff --git a/teslajsonpy/controller.py b/teslajsonpy/controller.py index 6bddd055..328f0593 100644 --- a/teslajsonpy/controller.py +++ b/teslajsonpy/controller.py @@ -845,6 +845,7 @@ def _process_websocket_message(self, data): 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" diff --git a/teslajsonpy/gps.py b/teslajsonpy/gps.py index 3a76ce9c..884505c9 100644 --- a/teslajsonpy/gps.py +++ b/teslajsonpy/gps.py @@ -62,9 +62,18 @@ async def async_update(self, wake_if_asleep=False) -> None: await super().async_update(wake_if_asleep=wake_if_asleep) data = self._controller.get_drive_params(self._id) if data: - self.__longitude = data["longitude"] - self.__latitude = data["latitude"] - self.__heading = data["heading"] + if data["native_location_supported"]: + self.__longitude = data["native_longitude"] + self.__latitude = data["native_latitude"] + self.__heading = ( + data["native_heading"] + if data.get("native_heading") + else data["heading"] + ) + else: + self.__longitude = data["longitude"] + self.__latitude = data["latitude"] + self.__heading = data["heading"] self.__speed = data["speed"] if data["speed"] else 0 @staticmethod From c414ae7712053e8a2114b8efe3d7c994bd42a16f Mon Sep 17 00:00:00 2001 From: "Alan D. Tse" Date: Thu, 27 Feb 2020 00:21:15 -0800 Subject: [PATCH 38/40] docs: fix property documentation --- teslajsonpy/charger.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/teslajsonpy/charger.py b/teslajsonpy/charger.py index c890cc1c..817818eb 100644 --- a/teslajsonpy/charger.py +++ b/teslajsonpy/charger.py @@ -202,22 +202,22 @@ def time_left(self) -> float: @property def added_range(self) -> float: - """Return the charging rate.""" + """Return the added range.""" return self.__added_range @property def charge_current_request(self) -> float: - """Return the charging rate.""" + """Return the requested current.""" return self.__charge_current_request @property def charger_actual_current(self) -> float: - """Return the charging rate.""" + """Return the actual current.""" return self.__charger_actual_current @property def charger_voltage(self) -> float: - """Return the charging rate.""" + """Return the voltage.""" return self.__charger_voltage @property From 249a25c1aeb1c4650de0d25a096c9b8292ae946d Mon Sep 17 00:00:00 2001 From: "Alan D. Tse" Date: Thu, 27 Feb 2020 00:22:28 -0800 Subject: [PATCH 39/40] feat: expose charge_energy_added --- teslajsonpy/charger.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/teslajsonpy/charger.py b/teslajsonpy/charger.py index 817818eb..f5ce36aa 100644 --- a/teslajsonpy/charger.py +++ b/teslajsonpy/charger.py @@ -155,6 +155,7 @@ def __init__(self, data: Dict, controller) -> None: self.uniq_name: Text = self._uniq_name() self.bin_type: hex = 0xC self.__added_range = None + self.__charge_energy_added = None self.__charging_rate = None self.__time_to_full = None self.__charge_current_request = None @@ -176,6 +177,7 @@ async def async_update(self, wake_if_asleep=False) -> None: if self.__rated else data["charge_miles_added_ideal"] ) + self.__charge_energy_added = data["charge_energy_added"] self.__charging_rate = data["charge_rate"] self.__time_to_full = data["time_to_full_charge"] self.__charge_current_request = data["charge_current_request"] @@ -220,6 +222,11 @@ def charger_voltage(self) -> float: """Return the voltage.""" return self.__charger_voltage + @property + def charge_energy_added(self) -> float: + """Return the energy added.""" + return self.__charge_energy_added + @property def device_class(self) -> Text: """Return the HA device class.""" From 0e116c366bc3ac4fdd8300f6781e2da8c551cf00 Mon Sep 17 00:00:00 2001 From: "Alan D. Tse" Date: Fri, 28 Feb 2020 01:19:06 -0800 Subject: [PATCH 40/40] feat: expose expiration for oauth --- teslajsonpy/connection.py | 24 ++++++++++++++++-------- teslajsonpy/controller.py | 16 ++++++++++++++-- 2 files changed, 30 insertions(+), 10 deletions(-) diff --git a/teslajsonpy/connection.py b/teslajsonpy/connection.py index 5cf9407c..84c71b50 100644 --- a/teslajsonpy/connection.py +++ b/teslajsonpy/connection.py @@ -32,6 +32,7 @@ def __init__( password: Text = None, access_token: Text = None, refresh_token: Text = None, + expiration: int = 0, ) -> None: """Initialize connection object.""" self.user_agent: Text = "Model S 2.1.79 (SM-G900V; Android REL 4.4.4; en_US" @@ -45,15 +46,15 @@ def __init__( self.websocket_url: Text = "wss://streaming.vn.teslamotors.com/streaming" self.api: Text = "/api/1/" self.oauth: Dict[Text, Text] = {} - self.expiration: int = 0 - self.access_token = None + self.expiration: int = expiration + self.access_token = access_token self.head = None self.refresh_token = refresh_token self.websession = websession self.token_refreshed = False self.generate_oauth(email, password, refresh_token) - if access_token: - self.__sethead(access_token) + if self.access_token: + self.__sethead(access_token=self.access_token, expiration=self.expiration) _LOGGER.debug("Connecting with existing access token") self.websocket = None @@ -104,7 +105,9 @@ async def post(self, command, method="post", data=None): "Requesting new oauth token using %s", self.oauth["grant_type"] ) auth = await self.__open("/oauth/token", "post", data=self.oauth) - self.__sethead(auth["access_token"], auth["expires_in"]) + self.__sethead( + access_token=auth["access_token"], expires_in=auth["expires_in"] + ) self.refresh_token = auth["refresh_token"] self.generate_oauth() self.token_refreshed = True @@ -112,11 +115,16 @@ async def post(self, command, method="post", data=None): f"{self.api}{command}", method=method, headers=self.head, data=data ) - def __sethead(self, access_token: Text, expires_in: int = 1800): + def __sethead( + self, access_token: Text, expires_in: int = 1800, expiration: int = 0 + ): """Set HTTP header.""" self.access_token = access_token - now = calendar.timegm(datetime.datetime.now().timetuple()) - self.expiration = now + expires_in + if expiration > 0: + self.expiration = expiration + else: + now = calendar.timegm(datetime.datetime.now().timetuple()) + self.expiration = now + expires_in self.head = { "Authorization": f"Bearer {access_token}", "User-Agent": self.user_agent, diff --git a/teslajsonpy/controller.py b/teslajsonpy/controller.py index 328f0593..366506c4 100644 --- a/teslajsonpy/controller.py +++ b/teslajsonpy/controller.py @@ -199,6 +199,7 @@ def __init__( password: Text = None, access_token: Text = None, refresh_token: Text = None, + expiration: int = 0, update_interval: int = 300, enable_websocket: bool = False, ) -> None: @@ -210,13 +211,14 @@ def __init__( 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, email, password, access_token, refresh_token + websession, email, password, access_token, refresh_token, expiration ) self.__components = [] self._update_interval: int = update_interval @@ -303,6 +305,7 @@ async def connect( 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): @@ -330,6 +333,15 @@ def get_tokens(self) -> Tuple[Text, Text]: self.__connection.token_refreshed = False return (self.__connection.refresh_token, self.__connection.access_token) + def get_expiration(self) -> int: + """Return expiration for oauth. + + Returns + int: Returns timestamp when oauth expires + + """ + return self.__connection.expiration + def register_websocket_callback(self, callback) -> int: """Register callback for websocket messages. @@ -601,7 +613,7 @@ def _calculate_next_interval(vin: int) -> int: async with self.__lock[vin]: car_state = self.car_state[vin].get("state") if ( - (online or (wake_if_asleep and car_state == "asleep")) + (online or (wake_if_asleep and car_state in ["asleep", "offline"])) and ( # pylint: disable=too-many-boolean-expressions self.__update.get(vin) )