Skip to content
This repository was archived by the owner on Feb 28, 2026. It is now read-only.

Commit 63a03a9

Browse files
Update to Home Assistant 2025.10.2 (#84)
* Update to Home Assistant 2025.10.2 * Add test suite --------- Co-authored-by: rikroe <42204099+rikroe@users.noreply.github.com>
1 parent adfb34c commit 63a03a9

38 files changed

Lines changed: 18466 additions & 138 deletions

custom_components/bmw_connected_drive/__init__.py

Lines changed: 29 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -2,24 +2,21 @@
22

33
from __future__ import annotations
44

5-
from dataclasses import dataclass
65
import logging
76

87
import voluptuous as vol
98

10-
from homeassistant.config_entries import ConfigEntry
119
from homeassistant.const import CONF_DEVICE_ID, CONF_ENTITY_ID, CONF_NAME, Platform
1210
from homeassistant.core import HomeAssistant, callback
1311
from homeassistant.helpers import (
12+
config_validation as cv,
1413
device_registry as dr,
1514
discovery,
1615
entity_registry as er,
17-
issue_registry as ir,
1816
)
19-
import homeassistant.helpers.config_validation as cv
2017

2118
from .const import ATTR_VIN, CONF_READ_ONLY, DOMAIN
22-
from .coordinator import BMWDataUpdateCoordinator
19+
from .coordinator import BMWConfigEntry, BMWDataUpdateCoordinator
2320

2421
_LOGGER = logging.getLogger(__name__)
2522

@@ -50,19 +47,9 @@
5047
SERVICE_UPDATE_STATE = "update_state"
5148

5249

53-
type BMWConfigEntry = ConfigEntry[BMWData]
54-
55-
56-
@dataclass
57-
class BMWData:
58-
"""Class to store BMW runtime data."""
59-
60-
coordinator: BMWDataUpdateCoordinator
61-
62-
6350
@callback
6451
def _async_migrate_options_from_data_if_missing(
65-
hass: HomeAssistant, entry: ConfigEntry
52+
hass: HomeAssistant, entry: BMWConfigEntry
6653
) -> None:
6754
data = dict(entry.data)
6855
options = dict(entry.options)
@@ -86,23 +73,29 @@ async def _async_migrate_entries(
8673
@callback
8774
def update_unique_id(entry: er.RegistryEntry) -> dict[str, str] | None:
8875
replacements = {
89-
"charging_level_hv": "fuel_and_battery.remaining_battery_percent",
90-
"fuel_percent": "fuel_and_battery.remaining_fuel_percent",
91-
"ac_current_limit": "charging_profile.ac_current_limit",
92-
"charging_start_time": "fuel_and_battery.charging_start_time",
93-
"charging_end_time": "fuel_and_battery.charging_end_time",
94-
"charging_status": "fuel_and_battery.charging_status",
95-
"charging_target": "fuel_and_battery.charging_target",
96-
"remaining_battery_percent": "fuel_and_battery.remaining_battery_percent",
97-
"remaining_range_total": "fuel_and_battery.remaining_range_total",
98-
"remaining_range_electric": "fuel_and_battery.remaining_range_electric",
99-
"remaining_range_fuel": "fuel_and_battery.remaining_range_fuel",
100-
"remaining_fuel": "fuel_and_battery.remaining_fuel",
101-
"remaining_fuel_percent": "fuel_and_battery.remaining_fuel_percent",
102-
"activity": "climate.activity",
76+
Platform.SENSOR.value: {
77+
"charging_level_hv": "fuel_and_battery.remaining_battery_percent",
78+
"fuel_percent": "fuel_and_battery.remaining_fuel_percent",
79+
"ac_current_limit": "charging_profile.ac_current_limit",
80+
"charging_start_time": "fuel_and_battery.charging_start_time",
81+
"charging_end_time": "fuel_and_battery.charging_end_time",
82+
"charging_status": "fuel_and_battery.charging_status",
83+
"charging_target": "fuel_and_battery.charging_target",
84+
"remaining_battery_percent": "fuel_and_battery.remaining_battery_percent",
85+
"remaining_range_total": "fuel_and_battery.remaining_range_total",
86+
"remaining_range_electric": "fuel_and_battery.remaining_range_electric",
87+
"remaining_range_fuel": "fuel_and_battery.remaining_range_fuel",
88+
"remaining_fuel": "fuel_and_battery.remaining_fuel",
89+
"remaining_fuel_percent": "fuel_and_battery.remaining_fuel_percent",
90+
"activity": "climate.activity",
91+
}
10392
}
104-
if (key := entry.unique_id.split("-")[-1]) in replacements:
105-
new_unique_id = entry.unique_id.replace(key, replacements[key])
93+
if (key := entry.unique_id.split("-")[-1]) in replacements.get(
94+
entry.domain, []
95+
):
96+
new_unique_id = entry.unique_id.replace(
97+
key, replacements[entry.domain][key]
98+
)
10699
_LOGGER.debug(
107100
"Migrating entity '%s' unique_id from '%s' to '%s'",
108101
entry.entity_id,
@@ -128,30 +121,21 @@ def update_unique_id(entry: er.RegistryEntry) -> dict[str, str] | None:
128121
return True
129122

130123

131-
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
124+
async def async_setup_entry(hass: HomeAssistant, entry: BMWConfigEntry) -> bool:
132125
"""Set up BMW Connected Drive from a config entry."""
133126

134-
ir.async_create_issue(
135-
hass,
136-
DOMAIN,
137-
"stop_using_custom_component",
138-
is_fixable=False,
139-
severity=ir.IssueSeverity.ERROR,
140-
translation_key="stop_using_custom_component",
141-
)
142-
143127
_async_migrate_options_from_data_if_missing(hass, entry)
144128

145129
await _async_migrate_entries(hass, entry)
146130

147131
# Set up one data coordinator per account/config entry
148132
coordinator = BMWDataUpdateCoordinator(
149133
hass,
150-
entry=entry,
134+
config_entry=entry,
151135
)
152136
await coordinator.async_config_entry_first_refresh()
153137

154-
entry.runtime_data = BMWData(coordinator)
138+
entry.runtime_data = coordinator
155139

156140
# Set up all platforms except notify
157141
await hass.config_entries.async_forward_entry_setups(
@@ -185,7 +169,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
185169
return True
186170

187171

188-
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
172+
async def async_unload_entry(hass: HomeAssistant, entry: BMWConfigEntry) -> bool:
189173
"""Unload a config entry."""
190174

191175
return await hass.config_entries.async_unload_platforms(

custom_components/bmw_connected_drive/binary_sensor.py

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,14 +18,16 @@
1818
BinarySensorEntityDescription,
1919
)
2020
from homeassistant.core import HomeAssistant, callback
21-
from homeassistant.helpers.entity_platform import AddEntitiesCallback
21+
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
2222
from homeassistant.util.unit_system import UnitSystem
2323

2424
from . import BMWConfigEntry
2525
from .const import UNIT_MAP
2626
from .coordinator import BMWDataUpdateCoordinator
2727
from .entity import BMWBaseEntity
2828

29+
PARALLEL_UPDATES = 0
30+
2931
_LOGGER = logging.getLogger(__name__)
3032

3133

@@ -198,10 +200,10 @@ class BMWBinarySensorEntityDescription(BinarySensorEntityDescription):
198200
async def async_setup_entry(
199201
hass: HomeAssistant,
200202
config_entry: BMWConfigEntry,
201-
async_add_entities: AddEntitiesCallback,
203+
async_add_entities: AddConfigEntryEntitiesCallback,
202204
) -> None:
203205
"""Set up the BMW binary sensors from config entry."""
204-
coordinator = config_entry.runtime_data.coordinator
206+
coordinator = config_entry.runtime_data
205207

206208
entities = [
207209
BMWBinarySensor(coordinator, vehicle, description, hass.config.units)

custom_components/bmw_connected_drive/button.py

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -14,14 +14,16 @@
1414
from homeassistant.components.button import ButtonEntity, ButtonEntityDescription
1515
from homeassistant.core import HomeAssistant
1616
from homeassistant.exceptions import HomeAssistantError
17-
from homeassistant.helpers.entity_platform import AddEntitiesCallback
17+
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
1818

19-
from . import BMWConfigEntry
19+
from . import DOMAIN, BMWConfigEntry
2020
from .entity import BMWBaseEntity
2121

2222
if TYPE_CHECKING:
2323
from .coordinator import BMWDataUpdateCoordinator
2424

25+
PARALLEL_UPDATES = 1
26+
2527
_LOGGER = logging.getLogger(__name__)
2628

2729

@@ -53,7 +55,6 @@ class BMWButtonEntityDescription(ButtonEntityDescription):
5355
BMWButtonEntityDescription(
5456
key="deactivate_air_conditioning",
5557
translation_key="deactivate_air_conditioning",
56-
name="Deactivate air conditioning",
5758
remote_function=lambda vehicle: vehicle.remote_services.trigger_remote_air_conditioning_stop(),
5859
is_available=lambda vehicle: vehicle.is_remote_climate_stop_enabled,
5960
),
@@ -68,10 +69,10 @@ class BMWButtonEntityDescription(ButtonEntityDescription):
6869
async def async_setup_entry(
6970
hass: HomeAssistant,
7071
config_entry: BMWConfigEntry,
71-
async_add_entities: AddEntitiesCallback,
72+
async_add_entities: AddConfigEntryEntitiesCallback,
7273
) -> None:
7374
"""Set up the BMW buttons from config entry."""
74-
coordinator = config_entry.runtime_data.coordinator
75+
coordinator = config_entry.runtime_data
7576

7677
entities: list[BMWButton] = []
7778

@@ -109,6 +110,10 @@ async def async_press(self) -> None:
109110
try:
110111
await self.entity_description.remote_function(self.vehicle)
111112
except MyBMWAPIError as ex:
112-
raise HomeAssistantError(ex) from ex
113+
raise HomeAssistantError(
114+
translation_domain=DOMAIN,
115+
translation_key="remote_service_error",
116+
translation_placeholders={"exception": str(ex)},
117+
) from ex
113118

114119
self.coordinator.async_update_listeners()

custom_components/bmw_connected_drive/config_flow.py

Lines changed: 34 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,6 @@
1818
from homeassistant.config_entries import (
1919
SOURCE_REAUTH,
2020
SOURCE_RECONFIGURE,
21-
ConfigEntry,
2221
ConfigFlow,
2322
ConfigFlowResult,
2423
OptionsFlow,
@@ -39,6 +38,7 @@
3938
CONF_READ_ONLY,
4039
CONF_REFRESH_TOKEN,
4140
)
41+
from .coordinator import BMWConfigEntry
4242

4343
DATA_SCHEMA = vol.Schema(
4444
{
@@ -53,6 +53,12 @@
5353
},
5454
extra=vol.REMOVE_EXTRA,
5555
)
56+
RECONFIGURE_SCHEMA = vol.Schema(
57+
{
58+
vol.Required(CONF_PASSWORD): str,
59+
},
60+
extra=vol.REMOVE_EXTRA,
61+
)
5662
CAPTCHA_SCHEMA = vol.Schema(
5763
{
5864
vol.Required(CONF_CAPTCHA_TOKEN): str,
@@ -97,9 +103,10 @@ class BMWConfigFlow(ConfigFlow, domain=DOMAIN):
97103

98104
VERSION = 1
99105

100-
data: dict[str, Any] = {}
101-
102-
_existing_entry_data: Mapping[str, Any] | None = None
106+
def __init__(self) -> None:
107+
"""Initialize the config flow."""
108+
self.data: dict[str, Any] = {}
109+
self._existing_entry_data: dict[str, Any] = {}
103110

104111
async def async_step_user(
105112
self, user_input: dict[str, Any] | None = None
@@ -111,9 +118,8 @@ async def async_step_user(
111118
unique_id = f"{user_input[CONF_REGION]}-{user_input[CONF_USERNAME]}"
112119
await self.async_set_unique_id(unique_id)
113120

114-
if self.source in {SOURCE_REAUTH, SOURCE_RECONFIGURE}:
115-
self._abort_if_unique_id_mismatch(reason="account_mismatch")
116-
else:
121+
# Unique ID cannot change for reauth/reconfigure
122+
if self.source not in {SOURCE_REAUTH, SOURCE_RECONFIGURE}:
117123
self._abort_if_unique_id_configured()
118124

119125
# Store user input for later use
@@ -166,19 +172,35 @@ async def async_step_user(
166172

167173
return self.async_show_form(step_id="user", data_schema=schema, errors=errors)
168174

175+
async def async_step_change_password(
176+
self, user_input: dict[str, Any] | None = None
177+
) -> ConfigFlowResult:
178+
"""Show the change password step."""
179+
if user_input is not None:
180+
return await self.async_step_user(self._existing_entry_data | user_input)
181+
182+
return self.async_show_form(
183+
step_id="change_password",
184+
data_schema=RECONFIGURE_SCHEMA,
185+
description_placeholders={
186+
CONF_USERNAME: self._existing_entry_data[CONF_USERNAME],
187+
CONF_REGION: self._existing_entry_data[CONF_REGION],
188+
},
189+
)
190+
169191
async def async_step_reauth(
170192
self, entry_data: Mapping[str, Any]
171193
) -> ConfigFlowResult:
172194
"""Handle configuration by re-auth."""
173-
self._existing_entry_data = entry_data
174-
return await self.async_step_user()
195+
self._existing_entry_data = dict(entry_data)
196+
return await self.async_step_change_password()
175197

176198
async def async_step_reconfigure(
177199
self, user_input: dict[str, Any] | None = None
178200
) -> ConfigFlowResult:
179201
"""Handle a reconfiguration flow initialized by the user."""
180-
self._existing_entry_data = self._get_reconfigure_entry().data
181-
return await self.async_step_user()
202+
self._existing_entry_data = dict(self._get_reconfigure_entry().data)
203+
return await self.async_step_change_password()
182204

183205
async def async_step_captcha(
184206
self, user_input: dict[str, Any] | None = None
@@ -199,7 +221,7 @@ async def async_step_captcha(
199221
@staticmethod
200222
@callback
201223
def async_get_options_flow(
202-
config_entry: ConfigEntry,
224+
config_entry: BMWConfigEntry,
203225
) -> BMWOptionsFlow:
204226
"""Return a MyBMW option flow."""
205227
return BMWOptionsFlow()

0 commit comments

Comments
 (0)