Home Assistant Unofficial Reference 2024.12.1
__init__.py
Go to the documentation of this file.
1 """Reads vehicle status from MyBMW portal."""
2 
3 from __future__ import annotations
4 
5 from dataclasses import dataclass
6 import logging
7 
8 import voluptuous as vol
9 
10 from homeassistant.config_entries import ConfigEntry
11 from homeassistant.const import CONF_DEVICE_ID, CONF_ENTITY_ID, CONF_NAME, Platform
12 from homeassistant.core import HomeAssistant, callback
13 from homeassistant.helpers import (
14  device_registry as dr,
15  discovery,
16  entity_registry as er,
17 )
19 
20 from .const import ATTR_VIN, CONF_READ_ONLY, DOMAIN
21 from .coordinator import BMWDataUpdateCoordinator
22 
23 _LOGGER = logging.getLogger(__name__)
24 
25 
26 SERVICE_SCHEMA = vol.Schema(
27  vol.Any(
28  {vol.Required(ATTR_VIN): cv.string},
29  {vol.Required(CONF_DEVICE_ID): cv.string},
30  )
31 )
32 
33 DEFAULT_OPTIONS = {
34  CONF_READ_ONLY: False,
35 }
36 
37 PLATFORMS = [
38  Platform.BINARY_SENSOR,
39  Platform.BUTTON,
40  Platform.DEVICE_TRACKER,
41  Platform.LOCK,
42  Platform.NOTIFY,
43  Platform.NUMBER,
44  Platform.SELECT,
45  Platform.SENSOR,
46  Platform.SWITCH,
47 ]
48 
49 SERVICE_UPDATE_STATE = "update_state"
50 
51 
52 type BMWConfigEntry = ConfigEntry[BMWData]
53 
54 
55 @dataclass
56 class BMWData:
57  """Class to store BMW runtime data."""
58 
59  coordinator: BMWDataUpdateCoordinator
60 
61 
62 @callback
64  hass: HomeAssistant, entry: ConfigEntry
65 ) -> None:
66  data = dict(entry.data)
67  options = dict(entry.options)
68 
69  if CONF_READ_ONLY in data or list(options) != list(DEFAULT_OPTIONS):
70  options = dict(
71  DEFAULT_OPTIONS,
72  **{k: v for k, v in options.items() if k in DEFAULT_OPTIONS},
73  )
74  options[CONF_READ_ONLY] = data.pop(CONF_READ_ONLY, False)
75 
76  hass.config_entries.async_update_entry(entry, data=data, options=options)
77 
78 
80  hass: HomeAssistant, config_entry: BMWConfigEntry
81 ) -> bool:
82  """Migrate old entry."""
83  entity_registry = er.async_get(hass)
84 
85  @callback
86  def update_unique_id(entry: er.RegistryEntry) -> dict[str, str] | None:
87  replacements = {
88  "charging_level_hv": "fuel_and_battery.remaining_battery_percent",
89  "fuel_percent": "fuel_and_battery.remaining_fuel_percent",
90  "ac_current_limit": "charging_profile.ac_current_limit",
91  "charging_start_time": "fuel_and_battery.charging_start_time",
92  "charging_end_time": "fuel_and_battery.charging_end_time",
93  "charging_status": "fuel_and_battery.charging_status",
94  "charging_target": "fuel_and_battery.charging_target",
95  "remaining_battery_percent": "fuel_and_battery.remaining_battery_percent",
96  "remaining_range_total": "fuel_and_battery.remaining_range_total",
97  "remaining_range_electric": "fuel_and_battery.remaining_range_electric",
98  "remaining_range_fuel": "fuel_and_battery.remaining_range_fuel",
99  "remaining_fuel": "fuel_and_battery.remaining_fuel",
100  "remaining_fuel_percent": "fuel_and_battery.remaining_fuel_percent",
101  "activity": "climate.activity",
102  }
103  if (key := entry.unique_id.split("-")[-1]) in replacements:
104  new_unique_id = entry.unique_id.replace(key, replacements[key])
105  _LOGGER.debug(
106  "Migrating entity '%s' unique_id from '%s' to '%s'",
107  entry.entity_id,
108  entry.unique_id,
109  new_unique_id,
110  )
111  if existing_entity_id := entity_registry.async_get_entity_id(
112  entry.domain, entry.platform, new_unique_id
113  ):
114  _LOGGER.debug(
115  "Cannot migrate to unique_id '%s', already exists for '%s'",
116  new_unique_id,
117  existing_entity_id,
118  )
119  return None
120  return {
121  "new_unique_id": new_unique_id,
122  }
123  return None
124 
125  await er.async_migrate_entries(hass, config_entry.entry_id, update_unique_id)
126 
127  return True
128 
129 
130 async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
131  """Set up BMW Connected Drive from a config entry."""
132 
134 
135  await _async_migrate_entries(hass, entry)
136 
137  # Set up one data coordinator per account/config entry
138  coordinator = BMWDataUpdateCoordinator(
139  hass,
140  entry=entry,
141  )
142  await coordinator.async_config_entry_first_refresh()
143 
144  entry.runtime_data = BMWData(coordinator)
145 
146  # Set up all platforms except notify
147  await hass.config_entries.async_forward_entry_setups(
148  entry, [platform for platform in PLATFORMS if platform != Platform.NOTIFY]
149  )
150 
151  # set up notify platform, no entry support for notify platform yet,
152  # have to use discovery to load platform.
153  hass.async_create_task(
154  discovery.async_load_platform(
155  hass,
156  Platform.NOTIFY,
157  DOMAIN,
158  {CONF_NAME: DOMAIN, CONF_ENTITY_ID: entry.entry_id},
159  {},
160  )
161  )
162 
163  # Clean up vehicles which are not assigned to the account anymore
164  account_vehicles = {(DOMAIN, v.vin) for v in coordinator.account.vehicles}
165  device_registry = dr.async_get(hass)
166  device_entries = dr.async_entries_for_config_entry(
167  device_registry, config_entry_id=entry.entry_id
168  )
169  for device in device_entries:
170  if not device.identifiers.intersection(account_vehicles):
171  device_registry.async_update_device(
172  device.id, remove_config_entry_id=entry.entry_id
173  )
174 
175  return True
176 
177 
178 async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
179  """Unload a config entry."""
180 
181  return await hass.config_entries.async_unload_platforms(
182  entry, [platform for platform in PLATFORMS if platform != Platform.NOTIFY]
183  )
None _async_migrate_options_from_data_if_missing(HomeAssistant hass, ConfigEntry entry)
Definition: __init__.py:65
bool async_setup_entry(HomeAssistant hass, ConfigEntry entry)
Definition: __init__.py:130
bool _async_migrate_entries(HomeAssistant hass, BMWConfigEntry config_entry)
Definition: __init__.py:81
bool async_unload_entry(HomeAssistant hass, ConfigEntry entry)
Definition: __init__.py:178
dict[str, str]|None update_unique_id(er.RegistryEntry entity_entry, str unique_id)
Definition: __init__.py:168