Home Assistant Unofficial Reference 2024.12.1
coordinator.py
Go to the documentation of this file.
1 """Coordinator for BMW."""
2 
3 from __future__ import annotations
4 
5 from datetime import timedelta
6 import logging
7 
8 from bimmer_connected.account import MyBMWAccount
9 from bimmer_connected.api.regions import get_region_from_name
10 from bimmer_connected.models import (
11  GPSPosition,
12  MyBMWAPIError,
13  MyBMWAuthError,
14  MyBMWCaptchaMissingError,
15 )
16 from httpx import RequestError
17 
18 from homeassistant.config_entries import ConfigEntry
19 from homeassistant.const import CONF_PASSWORD, CONF_REGION, CONF_USERNAME
20 from homeassistant.core import HomeAssistant
21 from homeassistant.exceptions import ConfigEntryAuthFailed
22 from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
23 from homeassistant.util.ssl import get_default_context
24 
25 from .const import CONF_GCID, CONF_READ_ONLY, CONF_REFRESH_TOKEN, DOMAIN, SCAN_INTERVALS
26 
27 _LOGGER = logging.getLogger(__name__)
28 
29 
31  """Class to manage fetching BMW data."""
32 
33  account: MyBMWAccount
34 
35  def __init__(self, hass: HomeAssistant, *, entry: ConfigEntry) -> None:
36  """Initialize account-wide BMW data updater."""
37  self.accountaccount = MyBMWAccount(
38  entry.data[CONF_USERNAME],
39  entry.data[CONF_PASSWORD],
40  get_region_from_name(entry.data[CONF_REGION]),
41  observer_position=GPSPosition(hass.config.latitude, hass.config.longitude),
42  verify=get_default_context(),
43  )
44  self.read_onlyread_only = entry.options[CONF_READ_ONLY]
45  self._entry_entry = entry
46 
47  if CONF_REFRESH_TOKEN in entry.data:
48  self.accountaccount.set_refresh_token(
49  refresh_token=entry.data[CONF_REFRESH_TOKEN],
50  gcid=entry.data.get(CONF_GCID),
51  )
52 
53  super().__init__(
54  hass,
55  _LOGGER,
56  name=f"{DOMAIN}-{entry.data['username']}",
57  update_interval=timedelta(seconds=SCAN_INTERVALS[entry.data[CONF_REGION]]),
58  )
59 
60  # Default to false on init so _async_update_data logic works
61  self.last_update_successlast_update_successlast_update_success = False
62 
63  async def _async_update_data(self) -> None:
64  """Fetch data from BMW."""
65  old_refresh_token = self.accountaccount.refresh_token
66 
67  try:
68  await self.accountaccount.get_vehicles()
69  except MyBMWCaptchaMissingError as err:
70  # If a captcha is required (user/password login flow), always trigger the reauth flow
72  translation_domain=DOMAIN,
73  translation_key="missing_captcha",
74  ) from err
75  except MyBMWAuthError as err:
76  # Allow one retry interval before raising AuthFailed to avoid flaky API issues
77  if self.last_update_successlast_update_successlast_update_success:
78  raise UpdateFailed(err) from err
79  # Clear refresh token and trigger reauth if previous update failed as well
80  self._update_config_entry_refresh_token_update_config_entry_refresh_token(None)
81  raise ConfigEntryAuthFailed(err) from err
82  except (MyBMWAPIError, RequestError) as err:
83  raise UpdateFailed(err) from err
84 
85  if self.accountaccount.refresh_token != old_refresh_token:
86  self._update_config_entry_refresh_token_update_config_entry_refresh_token(self.accountaccount.refresh_token)
87 
88  def _update_config_entry_refresh_token(self, refresh_token: str | None) -> None:
89  """Update or delete the refresh_token in the Config Entry."""
90  data = {
91  **self._entry_entry.data,
92  CONF_REFRESH_TOKEN: refresh_token,
93  }
94  if not refresh_token:
95  data.pop(CONF_REFRESH_TOKEN)
96  self.hasshass.config_entries.async_update_entry(self._entry_entry, data=data)
ssl.SSLContext get_default_context()
Definition: ssl.py:118