Home Assistant Unofficial Reference 2024.12.1
__init__.py
Go to the documentation of this file.
1 """Tesla Fleet integration."""
2 
3 import asyncio
4 from typing import Final
5 
6 from aiohttp.client_exceptions import ClientResponseError
7 import jwt
8 from tesla_fleet_api import (
9  EnergySpecific,
10  TeslaFleetApi,
11  VehicleSigned,
12  VehicleSpecific,
13 )
14 from tesla_fleet_api.const import Scope
15 from tesla_fleet_api.exceptions import (
16  InvalidRegion,
17  InvalidToken,
18  LibraryError,
19  LoginRequired,
20  OAuthExpired,
21  TeslaFleetError,
22 )
23 
24 from homeassistant.config_entries import ConfigEntry
25 from homeassistant.const import CONF_ACCESS_TOKEN, CONF_TOKEN, Platform
26 from homeassistant.core import HomeAssistant
27 from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady
28 from homeassistant.helpers import device_registry as dr
29 from homeassistant.helpers.aiohttp_client import async_get_clientsession
31  OAuth2Session,
32  async_get_config_entry_implementation,
33 )
35 from homeassistant.helpers.device_registry import DeviceInfo
36 
37 from .config_flow import OAuth2FlowHandler
38 from .const import DOMAIN, LOGGER, MODELS
39 from .coordinator import (
40  TeslaFleetEnergySiteInfoCoordinator,
41  TeslaFleetEnergySiteLiveCoordinator,
42  TeslaFleetVehicleDataCoordinator,
43 )
44 from .models import TeslaFleetData, TeslaFleetEnergyData, TeslaFleetVehicleData
45 from .oauth import TeslaSystemImplementation
46 
47 PLATFORMS: Final = [
48  Platform.BINARY_SENSOR,
49  Platform.BUTTON,
50  Platform.CLIMATE,
51  Platform.COVER,
52  Platform.DEVICE_TRACKER,
53  Platform.LOCK,
54  Platform.MEDIA_PLAYER,
55  Platform.NUMBER,
56  Platform.SELECT,
57  Platform.SENSOR,
58  Platform.SWITCH,
59 ]
60 
61 type TeslaFleetConfigEntry = ConfigEntry[TeslaFleetData]
62 
63 CONFIG_SCHEMA = cv.config_entry_only_config_schema(DOMAIN)
64 
65 
66 async def async_setup_entry(hass: HomeAssistant, entry: TeslaFleetConfigEntry) -> bool:
67  """Set up TeslaFleet config."""
68 
69  access_token = entry.data[CONF_TOKEN][CONF_ACCESS_TOKEN]
70  session = async_get_clientsession(hass)
71 
72  token = jwt.decode(access_token, options={"verify_signature": False})
73  scopes: list[Scope] = [Scope(s) for s in token["scp"]]
74  region: str = token["ou_code"].lower()
75 
76  OAuth2FlowHandler.async_register_implementation(
77  hass,
79  )
80 
81  implementation = await async_get_config_entry_implementation(hass, entry)
82  oauth_session = OAuth2Session(hass, entry, implementation)
83  refresh_lock = asyncio.Lock()
84 
85  async def _refresh_token() -> str:
86  async with refresh_lock:
87  try:
88  await oauth_session.async_ensure_token_valid()
89  except ClientResponseError as e:
90  if e.status == 401:
91  raise ConfigEntryAuthFailed from e
92  raise ConfigEntryNotReady from e
93  token: str = oauth_session.token[CONF_ACCESS_TOKEN]
94  return token
95 
96  # Create API connection
97  tesla = TeslaFleetApi(
98  session=session,
99  access_token=access_token,
100  region=region,
101  charging_scope=False,
102  partner_scope=False,
103  energy_scope=Scope.ENERGY_DEVICE_DATA in scopes,
104  vehicle_scope=Scope.VEHICLE_DEVICE_DATA in scopes,
105  refresh_hook=_refresh_token,
106  )
107  try:
108  products = (await tesla.products())["response"]
109  except (InvalidToken, OAuthExpired, LoginRequired) as e:
110  raise ConfigEntryAuthFailed from e
111  except InvalidRegion:
112  try:
113  LOGGER.warning("Region is invalid, trying to find the correct region")
114  await tesla.find_server()
115  try:
116  products = (await tesla.products())["response"]
117  except TeslaFleetError as e:
118  raise ConfigEntryNotReady from e
119  except LibraryError as e:
120  raise ConfigEntryAuthFailed from e
121  except TeslaFleetError as e:
122  raise ConfigEntryNotReady from e
123 
124  device_registry = dr.async_get(hass)
125 
126  # Create array of classes
127  vehicles: list[TeslaFleetVehicleData] = []
128  energysites: list[TeslaFleetEnergyData] = []
129  for product in products:
130  if "vin" in product and hasattr(tesla, "vehicle"):
131  # Remove the protobuff 'cached_data' that we do not use to save memory
132  product.pop("cached_data", None)
133  vin = product["vin"]
134  signing = product["command_signing"] == "required"
135  if signing:
136  if not tesla.private_key:
137  await tesla.get_private_key(hass.config.path("tesla_fleet.key"))
138  api = VehicleSigned(tesla.vehicle, vin)
139  else:
140  api = VehicleSpecific(tesla.vehicle, vin)
141  coordinator = TeslaFleetVehicleDataCoordinator(hass, api, product)
142 
143  await coordinator.async_config_entry_first_refresh()
144 
145  device = DeviceInfo(
146  identifiers={(DOMAIN, vin)},
147  manufacturer="Tesla",
148  name=product["display_name"],
149  model=MODELS.get(vin[3]),
150  serial_number=vin,
151  )
152 
153  vehicles.append(
155  api=api,
156  coordinator=coordinator,
157  vin=vin,
158  device=device,
159  signing=signing,
160  )
161  )
162  elif "energy_site_id" in product and hasattr(tesla, "energy"):
163  site_id = product["energy_site_id"]
164  if not (
165  product["components"]["battery"]
166  or product["components"]["solar"]
167  or "wall_connectors" in product["components"]
168  ):
169  LOGGER.debug(
170  "Skipping Energy Site %s as it has no components",
171  site_id,
172  )
173  continue
174 
175  api = EnergySpecific(tesla.energy, site_id)
176 
177  live_coordinator = TeslaFleetEnergySiteLiveCoordinator(hass, api)
178  info_coordinator = TeslaFleetEnergySiteInfoCoordinator(hass, api, product)
179 
180  await live_coordinator.async_config_entry_first_refresh()
181  await info_coordinator.async_config_entry_first_refresh()
182 
183  # Create energy site model
184  model = None
185  models = set()
186  for gateway in info_coordinator.data.get("components_gateways", []):
187  if gateway.get("part_name"):
188  models.add(gateway["part_name"])
189  for battery in info_coordinator.data.get("components_batteries", []):
190  if battery.get("part_name"):
191  models.add(battery["part_name"])
192  if models:
193  model = ", ".join(sorted(models))
194 
195  device = DeviceInfo(
196  identifiers={(DOMAIN, str(site_id))},
197  manufacturer="Tesla",
198  name=product.get("site_name", "Energy Site"),
199  model=model,
200  serial_number=str(site_id),
201  )
202 
203  # Create the energy site device regardless of it having entities
204  # This is so users with a Wall Connector but without a Powerwall can still make service calls
205  device_registry.async_get_or_create(
206  config_entry_id=entry.entry_id, **device
207  )
208 
209  energysites.append(
211  api=api,
212  live_coordinator=live_coordinator,
213  info_coordinator=info_coordinator,
214  id=site_id,
215  device=device,
216  )
217  )
218 
219  # Setup Platforms
220  entry.runtime_data = TeslaFleetData(vehicles, energysites, scopes)
221  await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
222 
223  return True
224 
225 
226 async def async_unload_entry(hass: HomeAssistant, entry: TeslaFleetConfigEntry) -> bool:
227  """Unload TeslaFleet Config."""
228  return await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
bool async_setup_entry(HomeAssistant hass, TeslaFleetConfigEntry entry)
Definition: __init__.py:66
bool async_unload_entry(HomeAssistant hass, TeslaFleetConfigEntry entry)
Definition: __init__.py:226
aiohttp.ClientSession async_get_clientsession(HomeAssistant hass, bool verify_ssl=True, socket.AddressFamily family=socket.AF_UNSPEC, ssl_util.SSLCipherList ssl_cipher=ssl_util.SSLCipherList.PYTHON_DEFAULT)
AbstractOAuth2Implementation async_get_config_entry_implementation(HomeAssistant hass, config_entries.ConfigEntry config_entry)