Home Assistant Unofficial Reference 2024.12.1
__init__.py
Go to the documentation of this file.
1 """Teslemetry integration."""
2 
3 import asyncio
4 from collections.abc import Callable
5 from typing import Final
6 
7 from tesla_fleet_api import EnergySpecific, Teslemetry, VehicleSpecific
8 from tesla_fleet_api.const import Scope
9 from tesla_fleet_api.exceptions import (
10  InvalidToken,
11  SubscriptionRequired,
12  TeslaFleetError,
13 )
14 from teslemetry_stream import TeslemetryStream
15 
16 from homeassistant.config_entries import ConfigEntry
17 from homeassistant.const import CONF_ACCESS_TOKEN, Platform
18 from homeassistant.core import HomeAssistant
19 from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady
20 from homeassistant.helpers import device_registry as dr
21 from homeassistant.helpers.aiohttp_client import async_get_clientsession
23 from homeassistant.helpers.device_registry import DeviceInfo
24 from homeassistant.helpers.typing import ConfigType
25 
26 from .const import DOMAIN, LOGGER, MODELS
27 from .coordinator import (
28  TeslemetryEnergyHistoryCoordinator,
29  TeslemetryEnergySiteInfoCoordinator,
30  TeslemetryEnergySiteLiveCoordinator,
31  TeslemetryVehicleDataCoordinator,
32 )
33 from .helpers import flatten
34 from .models import TeslemetryData, TeslemetryEnergyData, TeslemetryVehicleData
35 from .services import async_register_services
36 
37 PLATFORMS: Final = [
38  Platform.BINARY_SENSOR,
39  Platform.BUTTON,
40  Platform.CLIMATE,
41  Platform.COVER,
42  Platform.DEVICE_TRACKER,
43  Platform.LOCK,
44  Platform.MEDIA_PLAYER,
45  Platform.NUMBER,
46  Platform.SELECT,
47  Platform.SENSOR,
48  Platform.SWITCH,
49  Platform.UPDATE,
50 ]
51 
52 type TeslemetryConfigEntry = ConfigEntry[TeslemetryData]
53 
54 CONFIG_SCHEMA = cv.config_entry_only_config_schema(DOMAIN)
55 
56 
57 async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
58  """Set up the Telemetry integration."""
60  return True
61 
62 
63 async def async_setup_entry(hass: HomeAssistant, entry: TeslemetryConfigEntry) -> bool:
64  """Set up Teslemetry config."""
65 
66  access_token = entry.data[CONF_ACCESS_TOKEN]
67  session = async_get_clientsession(hass)
68 
69  # Create API connection
70  teslemetry = Teslemetry(
71  session=session,
72  access_token=access_token,
73  )
74  try:
75  calls = await asyncio.gather(
76  teslemetry.metadata(),
77  teslemetry.products(),
78  )
79  except InvalidToken as e:
80  raise ConfigEntryAuthFailed from e
81  except SubscriptionRequired as e:
82  raise ConfigEntryAuthFailed from e
83  except TeslaFleetError as e:
84  raise ConfigEntryNotReady from e
85 
86  scopes = calls[0]["scopes"]
87  region = calls[0]["region"]
88  products = calls[1]["response"]
89 
90  device_registry = dr.async_get(hass)
91 
92  # Create array of classes
93  vehicles: list[TeslemetryVehicleData] = []
94  energysites: list[TeslemetryEnergyData] = []
95 
96  # Create the stream
97  stream = TeslemetryStream(
98  session,
99  access_token,
100  server=f"{region.lower()}.teslemetry.com",
101  parse_timestamp=True,
102  )
103 
104  for product in products:
105  if "vin" in product and Scope.VEHICLE_DEVICE_DATA in scopes:
106  # Remove the protobuff 'cached_data' that we do not use to save memory
107  product.pop("cached_data", None)
108  vin = product["vin"]
109  api = VehicleSpecific(teslemetry.vehicle, vin)
110  coordinator = TeslemetryVehicleDataCoordinator(hass, api, product)
111  device = DeviceInfo(
112  identifiers={(DOMAIN, vin)},
113  manufacturer="Tesla",
114  configuration_url="https://teslemetry.com/console",
115  name=product["display_name"],
116  model=MODELS.get(vin[3]),
117  serial_number=vin,
118  )
119 
120  remove_listener = stream.async_add_listener(
121  create_handle_vehicle_stream(vin, coordinator),
122  {"vin": vin},
123  )
124 
125  vehicles.append(
127  api=api,
128  coordinator=coordinator,
129  stream=stream,
130  vin=vin,
131  device=device,
132  remove_listener=remove_listener,
133  )
134  )
135 
136  elif "energy_site_id" in product and Scope.ENERGY_DEVICE_DATA in scopes:
137  site_id = product["energy_site_id"]
138  powerwall = (
139  product["components"]["battery"] or product["components"]["solar"]
140  )
141  wall_connector = "wall_connectors" in product["components"]
142  if not powerwall and not wall_connector:
143  LOGGER.debug(
144  "Skipping Energy Site %s as it has no components",
145  site_id,
146  )
147  continue
148 
149  api = EnergySpecific(teslemetry.energy, site_id)
150  device = DeviceInfo(
151  identifiers={(DOMAIN, str(site_id))},
152  manufacturer="Tesla",
153  configuration_url="https://teslemetry.com/console",
154  name=product.get("site_name", "Energy Site"),
155  serial_number=str(site_id),
156  )
157 
158  energysites.append(
160  api=api,
161  live_coordinator=TeslemetryEnergySiteLiveCoordinator(hass, api),
162  info_coordinator=TeslemetryEnergySiteInfoCoordinator(
163  hass, api, product
164  ),
165  history_coordinator=(
167  if powerwall
168  else None
169  ),
170  id=site_id,
171  device=device,
172  )
173  )
174 
175  # Run all first refreshes
176  await asyncio.gather(
177  *(
178  vehicle.coordinator.async_config_entry_first_refresh()
179  for vehicle in vehicles
180  ),
181  *(
182  energysite.live_coordinator.async_config_entry_first_refresh()
183  for energysite in energysites
184  ),
185  *(
186  energysite.info_coordinator.async_config_entry_first_refresh()
187  for energysite in energysites
188  ),
189  *(
190  energysite.history_coordinator.async_config_entry_first_refresh()
191  for energysite in energysites
192  if energysite.history_coordinator
193  ),
194  )
195 
196  # Add energy device models
197  for energysite in energysites:
198  models = set()
199  for gateway in energysite.info_coordinator.data.get("components_gateways", []):
200  if gateway.get("part_name"):
201  models.add(gateway["part_name"])
202  for battery in energysite.info_coordinator.data.get("components_batteries", []):
203  if battery.get("part_name"):
204  models.add(battery["part_name"])
205  if models:
206  energysite.device["model"] = ", ".join(sorted(models))
207 
208  # Create the energy site device regardless of it having entities
209  # This is so users with a Wall Connector but without a Powerwall can still make service calls
210  device_registry.async_get_or_create(
211  config_entry_id=entry.entry_id, **energysite.device
212  )
213 
214  # Setup Platforms
215  entry.runtime_data = TeslemetryData(vehicles, energysites, scopes)
216  await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
217 
218  return True
219 
220 
221 async def async_unload_entry(hass: HomeAssistant, entry: TeslemetryConfigEntry) -> bool:
222  """Unload Teslemetry Config."""
223  return await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
224 
225 
226 async def async_migrate_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool:
227  """Migrate config entry."""
228  if config_entry.version > 1:
229  return False
230 
231  if config_entry.version == 1 and config_entry.minor_version < 2:
232  # Add unique_id to existing entry
233  teslemetry = Teslemetry(
234  session=async_get_clientsession(hass),
235  access_token=config_entry.data[CONF_ACCESS_TOKEN],
236  )
237  try:
238  metadata = await teslemetry.metadata()
239  except TeslaFleetError as e:
240  LOGGER.error(e.message)
241  return False
242 
243  hass.config_entries.async_update_entry(
244  config_entry, unique_id=metadata["uid"], version=1, minor_version=2
245  )
246  return True
247 
248 
249 def create_handle_vehicle_stream(vin: str, coordinator) -> Callable[[dict], None]:
250  """Create a handle vehicle stream function."""
251 
252  def handle_vehicle_stream(data: dict) -> None:
253  """Handle vehicle data from the stream."""
254  if "vehicle_data" in data:
255  LOGGER.debug("Streaming received vehicle data from %s", vin)
256  coordinator.updated_once = True
257  coordinator.async_set_updated_data(flatten(data["vehicle_data"]))
258  elif "state" in data:
259  LOGGER.debug("Streaming received state from %s", vin)
260  coordinator.data["state"] = data["state"]
261  coordinator.async_set_updated_data(coordinator.data)
262 
263  return handle_vehicle_stream
None async_register_services(HomeAssistant hass)
Definition: services.py:80
dict[str, Any] flatten(dict[str, Any] data, str|None parent=None)
Definition: coordinator.py:41
Callable[[dict], None] create_handle_vehicle_stream(str vin, coordinator)
Definition: __init__.py:249
bool async_setup_entry(HomeAssistant hass, TeslemetryConfigEntry entry)
Definition: __init__.py:63
bool async_setup(HomeAssistant hass, ConfigType config)
Definition: __init__.py:57
bool async_unload_entry(HomeAssistant hass, TeslemetryConfigEntry entry)
Definition: __init__.py:221
bool async_migrate_entry(HomeAssistant hass, ConfigEntry config_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)