class BMWDataUpdateCoordinator(DataUpdateCoordinator): """Class to manage fetching BMW data.""" account: MyBMWAccount def __init__(self, hass: HomeAssistant, *, entry: ConfigEntry) -> None: """Initialize account-wide BMW data updater.""" self.account = MyBMWAccount( entry.data[CONF_USERNAME], entry.data[CONF_PASSWORD], get_region_from_name(entry.data[CONF_REGION]), observer_position=GPSPosition(hass.config.latitude, hass.config.longitude), use_metric_units=hass.config.units.is_metric, ) self.read_only = entry.options[CONF_READ_ONLY] self._entry = entry if CONF_REFRESH_TOKEN in entry.data: self.account.set_refresh_token(entry.data[CONF_REFRESH_TOKEN]) super().__init__( hass, _LOGGER, name=f"{DOMAIN}-{entry.data['username']}", update_interval=SCAN_INTERVAL, ) async def _async_update_data(self) -> None: """Fetch data from BMW.""" old_refresh_token = self.account.refresh_token try: await self.account.get_vehicles() except (HTTPError, TimeoutException) as err: self._update_config_entry_refresh_token(None) raise UpdateFailed(f"Error communicating with BMW API: {err}") from err if self.account.refresh_token != old_refresh_token: self._update_config_entry_refresh_token(self.account.refresh_token) _LOGGER.debug( "bimmer_connected: refresh token %s > %s", old_refresh_token, self.account.refresh_token, ) def _update_config_entry_refresh_token(self, refresh_token: str | None) -> None: """Update or delete the refresh_token in the Config Entry.""" data = { **self._entry.data, CONF_REFRESH_TOKEN: refresh_token, } if not refresh_token: data.pop(CONF_REFRESH_TOKEN) self.hass.config_entries.async_update_entry(self._entry, data=data) def notify_listeners(self) -> None: """Notify all listeners to refresh HA state machine.""" for update_callback in self._listeners: update_callback()
async def mock_vehicles_from_fixture(account: MyBMWAccount) -> None: """Load MyBMWVehicle from fixtures and add them to the account.""" fixture_path = Path(get_fixture_path("", integration=BMW_DOMAIN)) fixture_vehicles_bmw = list(fixture_path.rglob("vehicles_v2_bmw_*.json")) fixture_vehicles_mini = list(fixture_path.rglob("vehicles_v2_mini_*.json")) # Load vehicle base lists as provided by vehicles/v2 API vehicles = { "bmw": [ vehicle for bmw_file in fixture_vehicles_bmw for vehicle in json.loads( load_fixture(bmw_file, integration=BMW_DOMAIN)) ], "mini": [ vehicle for mini_file in fixture_vehicles_mini for vehicle in json.loads( load_fixture(mini_file, integration=BMW_DOMAIN)) ], } fetched_at = utcnow() # simulate storing fingerprints if account.config.log_response_path: for brand in ["bmw", "mini"]: log_to_to_file( json.dumps(vehicles[brand]), account.config.log_response_path, f"vehicles_v2_{brand}", ) # Create a vehicle with base + specific state as provided by state/VIN API for vehicle_base in [ vehicle for brand in vehicles.values() for vehicle in brand ]: vehicle_state_path = (Path("vehicles") / vehicle_base["attributes"]["bodyType"] / f"state_{vehicle_base['vin']}_0.json") vehicle_state = json.loads( load_fixture( vehicle_state_path, integration=BMW_DOMAIN, )) account.add_vehicle( vehicle_base, vehicle_state, fetched_at, ) # simulate storing fingerprints if account.config.log_response_path: log_to_to_file( json.dumps(vehicle_state), account.config.log_response_path, f"state_{vehicle_base['vin']}", )
def __init__(self, hass: HomeAssistant, *, entry: ConfigEntry) -> None: """Initialize account-wide BMW data updater.""" self.account = MyBMWAccount( entry.data[CONF_USERNAME], entry.data[CONF_PASSWORD], get_region_from_name(entry.data[CONF_REGION]), observer_position=GPSPosition(hass.config.latitude, hass.config.longitude), use_metric_units=hass.config.units.is_metric, ) self.read_only = entry.options[CONF_READ_ONLY] self._entry = entry if CONF_REFRESH_TOKEN in entry.data: self.account.set_refresh_token(entry.data[CONF_REFRESH_TOKEN]) super().__init__( hass, _LOGGER, name=f"{DOMAIN}-{entry.data['username']}", update_interval=SCAN_INTERVAL, )
async def validate_input(hass: core.HomeAssistant, data: dict[str, Any]) -> dict[str, str]: """Validate the user input allows us to connect. Data has the keys from DATA_SCHEMA with values provided by the user. """ account = MyBMWAccount( data[CONF_USERNAME], data[CONF_PASSWORD], get_region_from_name(data[CONF_REGION]), ) try: await account.get_vehicles() except HTTPError as ex: raise CannotConnect from ex # Return info that you want to store in the config entry. return {"title": f"{data[CONF_USERNAME]}{data.get(CONF_SOURCE, '')}"}
async def collectData(user, password, region, attributes): # connect to bmw account = MyBMWAccount(user, password, get_region_from_name(region)) if not account: print('ERROR: Unable to log on') exit(1) # update states of all vehicles await account.get_vehicles() if DEBUG: print('DEBUG: Found {} vehicles: {}'.format( len(account.vehicles), ','.join([v.name for v in account.vehicles]))) # set gauges for each vehicle for vehicle in account.vehicles: vehicle_name = ''.join(e for e in vehicle.name if e.isalnum()) for attr in attributes: if attr['group']: vehicle_attribute_group = getattr(vehicle.status.vehicle, attr['group']) vehicle_attribute = getattr(vehicle_attribute_group, attr['name']) else: vehicle_attribute = getattr(vehicle.status.vehicle, attr['name']) if vehicle_attribute: if type( vehicle_attribute ) == ValueWithUnit: # some attributes are typed ValueWithUnit, some are just numbers vehicle_attribute_value = vehicle_attribute.value else: vehicle_attribute_value = vehicle_attribute if type(vehicle_attribute_value) == int or type( vehicle_attribute_value) == float: gauge = gauges[attr['name']] if gauge: gauge.labels( vehicle=vehicle_name, vin=vehicle.vin).set(vehicle_attribute_value)
class BMWDataUpdateCoordinator(DataUpdateCoordinator): """Class to manage fetching BMW data.""" account: MyBMWAccount def __init__(self, hass: HomeAssistant, *, entry: ConfigEntry) -> None: """Initialize account-wide BMW data updater.""" self.account = MyBMWAccount( entry.data[CONF_USERNAME], entry.data[CONF_PASSWORD], get_region_from_name(entry.data[CONF_REGION]), observer_position=GPSPosition(hass.config.latitude, hass.config.longitude), use_metric_units=hass.config.units.is_metric, ) self.read_only = entry.options[CONF_READ_ONLY] self._entry = entry if CONF_REFRESH_TOKEN in entry.data: self.account.set_refresh_token(entry.data[CONF_REFRESH_TOKEN]) super().__init__( hass, _LOGGER, name=f"{DOMAIN}-{entry.data['username']}", update_interval=SCAN_INTERVAL, ) async def _async_update_data(self) -> None: """Fetch data from BMW.""" old_refresh_token = self.account.refresh_token try: await self.account.get_vehicles() except (HTTPError, HTTPStatusError, TimeoutException) as err: if isinstance(err, HTTPStatusError) and err.response.status_code == 429: # Increase scan interval to not jump to not bring up the issue next time self.update_interval = timedelta( seconds=DEFAULT_SCAN_INTERVAL_SECONDS * 3) if isinstance(err, HTTPStatusError) and err.response.status_code in ( 401, 403, ): # Clear refresh token only on issues with authorization self._update_config_entry_refresh_token(None) raise UpdateFailed( f"Error communicating with BMW API: {err}") from err if self.account.refresh_token != old_refresh_token: self._update_config_entry_refresh_token(self.account.refresh_token) _LOGGER.debug( "bimmer_connected: refresh token %s > %s", old_refresh_token, self.account.refresh_token, ) # Reset scan interval after successful update self.update_interval = timedelta(seconds=DEFAULT_SCAN_INTERVAL_SECONDS) def _update_config_entry_refresh_token(self, refresh_token: str | None) -> None: """Update or delete the refresh_token in the Config Entry.""" data = { **self._entry.data, CONF_REFRESH_TOKEN: refresh_token, } if not refresh_token: data.pop(CONF_REFRESH_TOKEN) self.hass.config_entries.async_update_entry(self._entry, data=data)