Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: correctly represent online and asleep states according to API #373

Draft
wants to merge 1 commit into
base: dev
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 9 additions & 2 deletions teslajsonpy/car.py
Original file line number Diff line number Diff line change
Expand Up @@ -378,9 +378,14 @@ def is_trunk_closed(self) -> bool:

@property
def is_on(self) -> bool:
"""Return car is on."""
"""Return car is online (available, even if asleep)."""
return self._controller.is_car_online(vin=self.vin)

@property
def is_asleep(self) -> bool:
"""Return car is asleep."""
return self._controller.is_car_asleep(vin=self.vin)

@property
def longitude(self) -> float:
"""Return longitude."""
Expand Down Expand Up @@ -745,9 +750,10 @@ async def _send_command(
name, path_vars=path_vars, wake_if_asleep=wake_if_asleep, **kwargs
)
_LOGGER.debug("Response from command %s: %s", name, data)
self._controller.reset_tesla_exceptions(vin=self.vin)
return data
except TeslaException as ex:
if ex.code == 408 and not wake_if_asleep and not self.is_on:
if ex.code == 408 and not wake_if_asleep and self.is_asleep:
# 408 due to being asleep and we didn't try to wake it
_LOGGER.debug(
"Vehicle unavailable for command: %s, car state: %s, wake_if_asleep: %s",
Expand All @@ -756,6 +762,7 @@ async def _send_command(
wake_if_asleep,
)
return None
self._controller.count_tesla_exceptions(vin=self.vin)
raise ex

def _get_lat_long(self) -> Tuple[Optional[float], Optional[float]]:
Expand Down
6 changes: 6 additions & 0 deletions teslajsonpy/const.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,3 +30,9 @@
RESOURCE_TYPE = "resource_type"
RESOURCE_TYPE_SOLAR = "solar"
RESOURCE_TYPE_BATTERY = "battery"

STATUS_ONLINE = "online" # reported by Tesla, vehicle available and awake
STATUS_ASLEEP = "asleep" # reported by Tesla, vehicle available but asleep
STATUS_OFFLINE = "offline" # reported by Tesla, vehicle offline, wake/sleep unknown
STATUS_UNAVAILABLE = "unavailable" # set by Controller after successive api failures, overrides the above
STATUS_UNKNOWN = "unknown" # set by Controller during initialization, default value
182 changes: 113 additions & 69 deletions teslajsonpy/controller.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,10 @@
RESOURCE_TYPE,
RESOURCE_TYPE_SOLAR,
RESOURCE_TYPE_BATTERY,
STATUS_ONLINE,
STATUS_ASLEEP,
STATUS_UNKNOWN,
STATUS_UNAVAILABLE,
WAKE_TIMEOUT,
WAKE_CHECK_INTERVAL,
)
Expand Down Expand Up @@ -162,7 +166,8 @@ def __init__(
self.__lock = {}
self.__update_lock = None # controls access to update function
self.__wakeup_lock = {}
self.car_online = {}
self.__car_state: Dict[str, str] = {}
self.__car_sleeping: Dict[str, bool] = {}
self.__id_vin_map = {}
self.__vin_id_map = {}
self.__vin_vehicle_id_map = {}
Expand All @@ -178,6 +183,7 @@ def __init__(
self._include_energysites: bool = True
self._product_list: List[dict] = []
self._vehicle_list: List[dict] = []
self._consecutive_exceptions: Dict[str, int] = {}
self._vehicle_data: Dict[str, dict] = {}
self._energysite_list: List[dict] = []
self._site_config: Dict[int, dict] = {}
Expand Down Expand Up @@ -335,12 +341,14 @@ async def get_vehicle_data(self, vin: str, wake_if_asleep: bool = False) -> dict
wake_if_asleep=wake_if_asleep,
)
)["response"]
self.reset_tesla_exceptions(vin=vin)

except TeslaException as ex:
if ex.message == "VEHICLE_UNAVAILABLE":
_LOGGER.debug("Vehicle offline - data unavailable.")
return {}
raise ex
self.count_tesla_exceptions(vin=vin)
if should_giveup(ex):
raise ex
_LOGGER.debug("Unable to get vehicle data: %s: %s", ex.code, ex.message)
return {}

return response

Expand Down Expand Up @@ -402,21 +410,23 @@ async def generate_car_objects(
self._last_wake_up_time[vin] = 0
self.__update[vin] = True
self.__update_state[vin] = "normal"
self.set_car_online(vin=vin, online_status=car["state"] == "online")
self._consecutive_exceptions[vin] = 0
self._set_car_state(car.get("state", STATUS_UNKNOWN), vin=vin)
self.set_last_park_time(vin=vin, timestamp=self._last_attempted_update_time)
self.__driving[vin] = {}
self._vehicle_data[vin] = {}

try:
self._vehicle_data[vin] = await self.get_vehicle_data(
vin, wake_if_asleep=wake_if_asleep
)
except TeslaException as ex:
_LOGGER.warning(
"Unable to get vehicle data during setup, car will still be added. %s: %s",
ex.code,
ex.message,
)
if not self.is_car_asleep(vin=vin) or wake_if_asleep:
try:
self._vehicle_data[vin] = await self.get_vehicle_data(
vin, wake_if_asleep=wake_if_asleep
)
except TeslaException as ex:
_LOGGER.warning(
"Unable to get vehicle data during setup, car will still be added. %s: %s",
ex.code,
ex.message,
)
self.cars[vin] = TeslaCar(car, self, self._vehicle_data[vin])

return self.cars
Expand Down Expand Up @@ -511,27 +521,35 @@ async def wake_up(self, car_id) -> bool:
result = await self.api(
"WAKE_UP", path_vars={"vehicle_id": car_id}, wake_if_asleep=False
)
state = result.get("response", {}).get("state")
self.set_car_online(
car_id=car_id,
online_status=state == "online",
)
while not self.is_car_online(vin=car_vin) and time.time() < wake_deadline:
self._set_car_state(result.get("response", {}).get("state"), car_id=car_id)
while self.is_car_asleep(vin=car_vin) and time.time() < wake_deadline:
await asyncio.sleep(WAKE_CHECK_INTERVAL)
response = await self.get_vehicle_summary(vin=car_vin)
state = response.get("state")
self.set_car_online(
car_id=car_id,
online_status=state == "online",
)
self._set_car_state(response.get("state"), car_id=car_id)

_LOGGER.debug(
"%s: Wakeup took %d seconds, state: %s",
car_vin[-5:],
time.time() - wake_start_time,
state,
self.get_car_state(vin=car_vin),
)
return self.is_car_online(vin=car_vin)
return not self.is_car_asleep(vin=car_vin)

def count_tesla_exceptions(self, vin: str) -> None:
"""Keep track of consecutive exceptions."""
if vin not in self._consecutive_exceptions:
self._consecutive_exceptions[vin] = 0
self._consecutive_exceptions[vin] += 1
_LOGGER.debug(
"%s: exception counter increased to %d",
vin[-5:],
self._consecutive_exceptions[vin],
)

def reset_tesla_exceptions(self, vin: str) -> None:
"""Reset consecutive exception counter."""
self._consecutive_exceptions[vin] = 0
_LOGGER.debug("%s: exception counter reset", vin[-5:])

def _calculate_next_interval(self, vin: Text) -> int:
cur_time = round(time.time())
Expand All @@ -552,7 +570,7 @@ def _calculate_next_interval(self, vin: Text) -> int:
if vin not in self.__update_state:
self.__update_state[vin] = "normal"

if self.cars[vin].state == "asleep" or self.cars[vin].shift_state:
if self.is_car_asleep(vin=vin) or self.cars[vin].shift_state:
self.set_last_park_time(
vin=vin, timestamp=cur_time, shift_state=self.cars[vin].shift_state
)
Expand Down Expand Up @@ -781,10 +799,8 @@ async def _get_and_process_battery_summary(
self.set_vehicle_id_vin(
vehicle_id=car["vehicle_id"], vin=car["vin"]
)
self.set_car_online(
vin=car["vin"], online_status=car["state"] == "online"
)
self.cars[car["vin"]].update_car_info(car)
self._set_car_state(car["state"], vin=car["vin"])
self._last_attempted_update_time = cur_time

# Only update online vehicles that haven't been updated recently
Expand All @@ -794,26 +810,20 @@ async def _get_and_process_battery_summary(
car_id = self._update_id(car_id)
car_vin = self._id_to_vin(car_id)

for vin, online in self.get_car_online().items():
for vin, car in self.cars.items():
# If specific car_id provided, only update match
if (
(car_vin and car_vin != vin)
or vin not in self.__lock
or (vin and self.cars[vin].in_service)
or (vin and car.in_service)
):
continue

async with self.__lock[vin]:
if (
(
online
or (
wake_if_asleep
and self.cars[vin].state in ["asleep", "offline"]
)
)
and ( # pylint: disable=too-many-boolean-expressions
self.__update.get(vin)
(not self.is_car_asleep(vin=vin) or wake_if_asleep)
and self.__update.get(
vin
) # Only update cars with update flag on
and (
force
Expand All @@ -833,7 +843,7 @@ async def _get_and_process_battery_summary(
"Last wake up %s ago. "
),
vin[-5:],
self.cars[vin].state,
car.state,
self.__update.get(vin),
cur_time - self._last_update_time[vin],
cur_time - self.get_last_park_time(vin=vin),
Expand Down Expand Up @@ -1036,41 +1046,54 @@ def set_last_wake_up_time(
_LOGGER.debug("%s: Resetting last_wake_up_time to: %s", vin[-5:], timestamp)
self._last_wake_up_time[vin] = timestamp

def set_car_online(
self, car_id: Text = None, vin: Text = None, online_status: bool = True
def _set_car_state(
self, new_state: str, car_id: Text = None, vin: Text = None
) -> None:
"""Set online status for car_id.
"""Set vehicle state for car_id or vin.

Will also update "last_wake_up_time" if the car changes from offline
Will also update "last_wake_up_time" if the car changes from asleep
to online

Parameters
----------
new_state: string
The state string from a vehicle api response
car_id : string
Identifier for the car on the owner-api endpoint.
vin : string
VIN number

online_status : boolean
True if the car is online (awake)
False if the car is offline (out of reach or sleeping)


"""
if not (car_id or vin):
raise ValueError("Either car_id or vin must be provided to set car state")
if car_id and not vin:
vin = self._id_to_vin(car_id)
if vin and self.get_car_online(vin=vin) != online_status:

if self._consecutive_exceptions.get(vin, 0) >= 5 and new_state == STATUS_ONLINE:
# Tesla is clearly lying that it's online, set it as unavailable instead
new_state = STATUS_UNAVAILABLE

if self.get_car_state(vin=vin) != new_state:
_LOGGER.debug(
"%s: Changing car_online from %s to %s",
"%s: Changing car state from %s to %s",
vin[-5:],
self.get_car_online(vin=vin),
online_status,
self.get_car_state(vin=vin),
new_state,
)
self.car_online[vin] = online_status
if online_status:
self.set_last_wake_up_time(vin=vin, timestamp=round(time.time()))

def get_car_online(self, car_id: Text = None, vin: Text = None):
self.__car_state[vin] = new_state
# Update the car object's state as well if it exists
if vin in self.cars:
self.cars[vin].update_car_info({"state": new_state, "vin": vin})

if self.is_car_asleep(vin=vin) and new_state == STATUS_ONLINE:
self.set_last_wake_up_time(vin=vin, timestamp=round(time.time()))
# Don't update __car_sleeping when state is offline or unavailable
if new_state == STATUS_ASLEEP:
self.__car_sleeping[vin] = True
if new_state == STATUS_ONLINE:
self.__car_sleeping[vin] = False

def get_car_state(self, car_id: Text = None, vin: Text = None):
"""Get online status for car_id or all cars.

Parameters
Expand All @@ -1094,13 +1117,34 @@ def get_car_online(self, car_id: Text = None, vin: Text = None):
"""
if car_id and not vin:
vin = self._id_to_vin(car_id)
if vin and vin in self.car_online:
return self.car_online[vin]
return self.car_online
if vin:
return self.__car_state.get(vin, STATUS_UNKNOWN)
return self.__car_state

def is_car_online(self, car_id: Text = None, vin: Text = None) -> bool:
"""Alias for get_car_online for better readability."""
return self.get_car_online(car_id=car_id, vin=vin)
"""Get online status for car_id or vin.

Returns True if it is reachable, this includes asleep.
"""
if not (car_id or vin):
raise ValueError("Either car_id or vin must be provided")

return self.get_car_state(car_id=car_id, vin=vin) in [
STATUS_ONLINE,
STATUS_ASLEEP,
]

def is_car_asleep(self, car_id: Text = None, vin: Text = None) -> bool:
"""Get asleep status for car_id or vin.

Returns True if it is asleep, or was last seen as asleep.
"""
if not (car_id or vin):
raise ValueError("Either car_id or vin must be provided")
if car_id and not vin:
vin = self._id_to_vin(car_id)

return self.__car_sleeping.get(vin)

def set_id_vin(self, car_id: Text, vin: Text) -> None:
"""Update mappings of car_id <--> vin."""
Expand Down Expand Up @@ -1328,7 +1372,7 @@ async def api(
"wake_if_asleep only supported on endpoints with 'vehicle_id' path variable"
)
# If we already know the car is asleep, go ahead and wake it
if not self.is_car_online(car_id=car_id):
if self.is_car_asleep(car_id=car_id):
await self.wake_up(car_id=car_id)
return await self.__connection.post(
"", method=method, data=kwargs, url=uri
Expand Down
Loading