Home Assistant Unofficial Reference 2024.12.1
coordinator.py
Go to the documentation of this file.
1 """Tesla Fleet Data Coordinator."""
2 
3 from datetime import datetime, timedelta
4 from typing import Any
5 
6 from tesla_fleet_api import EnergySpecific, VehicleSpecific
7 from tesla_fleet_api.const import VehicleDataEndpoint
8 from tesla_fleet_api.exceptions import (
9  InvalidToken,
10  LoginRequired,
11  OAuthExpired,
12  RateLimited,
13  TeslaFleetError,
14  VehicleOffline,
15 )
16 from tesla_fleet_api.ratecalculator import RateCalculator
17 
18 from homeassistant.core import HomeAssistant
19 from homeassistant.exceptions import ConfigEntryAuthFailed
20 from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
21 
22 from .const import LOGGER, TeslaFleetState
23 
24 VEHICLE_INTERVAL_SECONDS = 90
25 VEHICLE_INTERVAL = timedelta(seconds=VEHICLE_INTERVAL_SECONDS)
26 VEHICLE_WAIT = timedelta(minutes=15)
27 
28 ENERGY_INTERVAL_SECONDS = 60
29 ENERGY_INTERVAL = timedelta(seconds=ENERGY_INTERVAL_SECONDS)
30 
31 ENDPOINTS = [
32  VehicleDataEndpoint.CHARGE_STATE,
33  VehicleDataEndpoint.CLIMATE_STATE,
34  VehicleDataEndpoint.DRIVE_STATE,
35  VehicleDataEndpoint.LOCATION_DATA,
36  VehicleDataEndpoint.VEHICLE_STATE,
37  VehicleDataEndpoint.VEHICLE_CONFIG,
38 ]
39 
40 
41 def flatten(data: dict[str, Any], parent: str | None = None) -> dict[str, Any]:
42  """Flatten the data structure."""
43  result = {}
44  for key, value in data.items():
45  if parent:
46  key = f"{parent}_{key}"
47  if isinstance(value, dict):
48  result.update(flatten(value, key))
49  else:
50  result[key] = value
51  return result
52 
53 
55  """Class to manage fetching data from the TeslaFleet API."""
56 
57  updated_once: bool
58  pre2021: bool
59  last_active: datetime
60  rate: RateCalculator
61 
62  def __init__(
63  self, hass: HomeAssistant, api: VehicleSpecific, product: dict
64  ) -> None:
65  """Initialize TeslaFleet Vehicle Update Coordinator."""
66  super().__init__(
67  hass,
68  LOGGER,
69  name="Tesla Fleet Vehicle",
70  update_interval=VEHICLE_INTERVAL,
71  )
72  self.apiapi = api
73  self.datadatadata = flatten(product)
74  self.updated_onceupdated_once = False
75  self.last_activelast_active = datetime.now()
76  self.raterate = RateCalculator(200, 86400, VEHICLE_INTERVAL_SECONDS, 3600, 5)
77 
78  async def _async_update_data(self) -> dict[str, Any]:
79  """Update vehicle data using TeslaFleet API."""
80 
81  try:
82  # Check if the vehicle is awake using a non-rate limited API call
83  if self.datadatadata["state"] != TeslaFleetState.ONLINE:
84  response = await self.apiapi.vehicle()
85  self.datadatadata["state"] = response["response"]["state"]
86 
87  if self.datadatadata["state"] != TeslaFleetState.ONLINE:
88  return self.datadatadata
89 
90  # This is a rated limited API call
91  self.raterate.consume()
92  response = await self.apiapi.vehicle_data(endpoints=ENDPOINTS)
93  data = response["response"]
94 
95  except VehicleOffline:
96  self.datadatadata["state"] = TeslaFleetState.ASLEEP
97  return self.datadatadata
98  except RateLimited as e:
99  LOGGER.warning(
100  "%s rate limited, will retry in %s seconds",
101  self.namename,
102  e.data.get("after"),
103  )
104  if "after" in e.data:
105  self.update_intervalupdate_intervalupdate_intervalupdate_intervalupdate_interval = timedelta(seconds=int(e.data["after"]))
106  return self.datadatadata
107  except (InvalidToken, OAuthExpired, LoginRequired) as e:
108  raise ConfigEntryAuthFailed from e
109  except TeslaFleetError as e:
110  raise UpdateFailed(e.message) from e
111 
112  # Calculate ideal refresh interval
113  self.update_intervalupdate_intervalupdate_intervalupdate_intervalupdate_interval = timedelta(seconds=self.raterate.calculate())
114 
115  self.updated_onceupdated_once = True
116 
117  if self.apiapi.pre2021 and data["state"] == TeslaFleetState.ONLINE:
118  # Handle pre-2021 vehicles which cannot sleep by themselves
119  if (
120  data["charge_state"].get("charging_state") == "Charging"
121  or data["vehicle_state"].get("is_user_present")
122  or data["vehicle_state"].get("sentry_mode")
123  ):
124  # Vehicle is active, reset timer
125  self.last_activelast_active = datetime.now()
126  else:
127  elapsed = datetime.now() - self.last_activelast_active
128  if elapsed > timedelta(minutes=20):
129  # Vehicle didn't sleep, try again in 15 minutes
130  self.last_activelast_active = datetime.now()
131  elif elapsed > timedelta(minutes=15):
132  # Let vehicle go to sleep now
133  self.update_intervalupdate_intervalupdate_intervalupdate_intervalupdate_interval = VEHICLE_WAIT
134 
135  return flatten(data)
136 
137 
139  """Class to manage fetching energy site live status from the TeslaFleet API."""
140 
141  updated_once: bool
142 
143  def __init__(self, hass: HomeAssistant, api: EnergySpecific) -> None:
144  """Initialize TeslaFleet Energy Site Live coordinator."""
145  super().__init__(
146  hass,
147  LOGGER,
148  name="Tesla Fleet Energy Site Live",
149  update_interval=timedelta(seconds=10),
150  )
151  self.apiapi = api
152  self.datadatadata = {}
153  self.updated_onceupdated_once = False
154 
155  async def _async_update_data(self) -> dict[str, Any]:
156  """Update energy site data using TeslaFleet API."""
157 
159 
160  try:
161  data = (await self.apiapi.live_status())["response"]
162  except RateLimited as e:
163  LOGGER.warning(
164  "%s rate limited, will retry in %s seconds",
165  self.namename,
166  e.data.get("after"),
167  )
168  if "after" in e.data:
169  self.update_intervalupdate_intervalupdate_intervalupdate_intervalupdate_interval = timedelta(seconds=int(e.data["after"]))
170  return self.datadatadata
171  except (InvalidToken, OAuthExpired, LoginRequired) as e:
172  raise ConfigEntryAuthFailed from e
173  except TeslaFleetError as e:
174  raise UpdateFailed(e.message) from e
175 
176  # Convert Wall Connectors from array to dict
177  data["wall_connectors"] = {
178  wc["din"]: wc for wc in (data.get("wall_connectors") or [])
179  }
180 
181  self.updated_onceupdated_once = True
182  return data
183 
184 
186  """Class to manage fetching energy site info from the TeslaFleet API."""
187 
188  updated_once: bool
189 
190  def __init__(self, hass: HomeAssistant, api: EnergySpecific, product: dict) -> None:
191  """Initialize TeslaFleet Energy Info coordinator."""
192  super().__init__(
193  hass,
194  LOGGER,
195  name="Tesla Fleet Energy Site Info",
196  update_interval=timedelta(seconds=15),
197  )
198  self.apiapi = api
199  self.datadatadata = flatten(product)
200  self.updated_onceupdated_once = False
201 
202  async def _async_update_data(self) -> dict[str, Any]:
203  """Update energy site data using TeslaFleet API."""
204 
206 
207  try:
208  data = (await self.apiapi.site_info())["response"]
209  except RateLimited as e:
210  LOGGER.warning(
211  "%s rate limited, will retry in %s seconds",
212  self.namename,
213  e.data.get("after"),
214  )
215  if "after" in e.data:
216  self.update_intervalupdate_intervalupdate_intervalupdate_intervalupdate_interval = timedelta(seconds=int(e.data["after"]))
217  return self.datadatadata
218  except (InvalidToken, OAuthExpired, LoginRequired) as e:
219  raise ConfigEntryAuthFailed from e
220  except TeslaFleetError as e:
221  raise UpdateFailed(e.message) from e
222 
223  self.updated_onceupdated_once = True
224  return flatten(data)
None __init__(self, HomeAssistant hass, EnergySpecific api, dict product)
Definition: coordinator.py:190
None __init__(self, HomeAssistant hass, VehicleSpecific api, dict product)
Definition: coordinator.py:64
web.Response get(self, web.Request request, str config_key)
Definition: view.py:88
dict[str, Any] flatten(dict[str, Any] data, str|None parent=None)
Definition: coordinator.py:41