Skip to content

Commit b4a4e21

Browse files
liudgerNoRi2909
andauthored
Add re-authentication to BSBLan (home-assistant#146280)
Co-authored-by: Norbert Rittel <[email protected]>
1 parent fb2d62d commit b4a4e21

File tree

5 files changed

+577
-43
lines changed

5 files changed

+577
-43
lines changed

homeassistant/components/bsblan/config_flow.py

Lines changed: 167 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,10 @@
22

33
from __future__ import annotations
44

5+
from collections.abc import Mapping
56
from typing import Any
67

7-
from bsblan import BSBLAN, BSBLANConfig, BSBLANError
8+
from bsblan import BSBLAN, BSBLANAuthError, BSBLANConfig, BSBLANError
89
import voluptuous as vol
910

1011
from homeassistant.config_entries import ConfigFlow, ConfigFlowResult
@@ -45,7 +46,7 @@ async def async_step_user(
4546
self.username = user_input.get(CONF_USERNAME)
4647
self.password = user_input.get(CONF_PASSWORD)
4748

48-
return await self._validate_and_create()
49+
return await self._validate_and_create(user_input)
4950

5051
async def async_step_zeroconf(
5152
self, discovery_info: ZeroconfServiceInfo
@@ -128,14 +129,29 @@ async def async_step_discovery_confirm(
128129
self.username = user_input.get(CONF_USERNAME)
129130
self.password = user_input.get(CONF_PASSWORD)
130131

131-
return await self._validate_and_create(is_discovery=True)
132+
return await self._validate_and_create(user_input, is_discovery=True)
132133

133134
async def _validate_and_create(
134-
self, is_discovery: bool = False
135+
self, user_input: dict[str, Any], is_discovery: bool = False
135136
) -> ConfigFlowResult:
136137
"""Validate device connection and create entry."""
137138
try:
138-
await self._get_bsblan_info(is_discovery=is_discovery)
139+
await self._get_bsblan_info()
140+
except BSBLANAuthError:
141+
if is_discovery:
142+
return self.async_show_form(
143+
step_id="discovery_confirm",
144+
data_schema=vol.Schema(
145+
{
146+
vol.Optional(CONF_PASSKEY): str,
147+
vol.Optional(CONF_USERNAME): str,
148+
vol.Optional(CONF_PASSWORD): str,
149+
}
150+
),
151+
errors={"base": "invalid_auth"},
152+
description_placeholders={"host": str(self.host)},
153+
)
154+
return self._show_setup_form({"base": "invalid_auth"}, user_input)
139155
except BSBLANError:
140156
if is_discovery:
141157
return self.async_show_form(
@@ -154,18 +170,145 @@ async def _validate_and_create(
154170

155171
return self._async_create_entry()
156172

173+
async def async_step_reauth(
174+
self, entry_data: Mapping[str, Any]
175+
) -> ConfigFlowResult:
176+
"""Handle reauth flow."""
177+
return await self.async_step_reauth_confirm()
178+
179+
async def async_step_reauth_confirm(
180+
self, user_input: dict[str, Any] | None = None
181+
) -> ConfigFlowResult:
182+
"""Handle reauth confirmation flow."""
183+
existing_entry = self.hass.config_entries.async_get_entry(
184+
self.context["entry_id"]
185+
)
186+
assert existing_entry
187+
188+
if user_input is None:
189+
# Preserve existing values as defaults
190+
return self.async_show_form(
191+
step_id="reauth_confirm",
192+
data_schema=vol.Schema(
193+
{
194+
vol.Optional(
195+
CONF_PASSKEY,
196+
default=existing_entry.data.get(
197+
CONF_PASSKEY, vol.UNDEFINED
198+
),
199+
): str,
200+
vol.Optional(
201+
CONF_USERNAME,
202+
default=existing_entry.data.get(
203+
CONF_USERNAME, vol.UNDEFINED
204+
),
205+
): str,
206+
vol.Optional(
207+
CONF_PASSWORD,
208+
default=vol.UNDEFINED,
209+
): str,
210+
}
211+
),
212+
)
213+
214+
# Use existing host and port, update auth credentials
215+
self.host = existing_entry.data[CONF_HOST]
216+
self.port = existing_entry.data[CONF_PORT]
217+
self.passkey = user_input.get(CONF_PASSKEY) or existing_entry.data.get(
218+
CONF_PASSKEY
219+
)
220+
self.username = user_input.get(CONF_USERNAME) or existing_entry.data.get(
221+
CONF_USERNAME
222+
)
223+
self.password = user_input.get(CONF_PASSWORD)
224+
225+
try:
226+
await self._get_bsblan_info(raise_on_progress=False, is_reauth=True)
227+
except BSBLANAuthError:
228+
return self.async_show_form(
229+
step_id="reauth_confirm",
230+
data_schema=vol.Schema(
231+
{
232+
vol.Optional(
233+
CONF_PASSKEY,
234+
default=user_input.get(CONF_PASSKEY, vol.UNDEFINED),
235+
): str,
236+
vol.Optional(
237+
CONF_USERNAME,
238+
default=user_input.get(CONF_USERNAME, vol.UNDEFINED),
239+
): str,
240+
vol.Optional(
241+
CONF_PASSWORD,
242+
default=vol.UNDEFINED,
243+
): str,
244+
}
245+
),
246+
errors={"base": "invalid_auth"},
247+
)
248+
except BSBLANError:
249+
return self.async_show_form(
250+
step_id="reauth_confirm",
251+
data_schema=vol.Schema(
252+
{
253+
vol.Optional(
254+
CONF_PASSKEY,
255+
default=user_input.get(CONF_PASSKEY, vol.UNDEFINED),
256+
): str,
257+
vol.Optional(
258+
CONF_USERNAME,
259+
default=user_input.get(CONF_USERNAME, vol.UNDEFINED),
260+
): str,
261+
vol.Optional(
262+
CONF_PASSWORD,
263+
default=vol.UNDEFINED,
264+
): str,
265+
}
266+
),
267+
errors={"base": "cannot_connect"},
268+
)
269+
270+
# Update the config entry with new auth data
271+
data_updates = {}
272+
if self.passkey is not None:
273+
data_updates[CONF_PASSKEY] = self.passkey
274+
if self.username is not None:
275+
data_updates[CONF_USERNAME] = self.username
276+
if self.password is not None:
277+
data_updates[CONF_PASSWORD] = self.password
278+
279+
return self.async_update_reload_and_abort(
280+
existing_entry, data_updates=data_updates, reason="reauth_successful"
281+
)
282+
157283
@callback
158-
def _show_setup_form(self, errors: dict | None = None) -> ConfigFlowResult:
284+
def _show_setup_form(
285+
self, errors: dict | None = None, user_input: dict[str, Any] | None = None
286+
) -> ConfigFlowResult:
159287
"""Show the setup form to the user."""
288+
# Preserve user input if provided, otherwise use defaults
289+
defaults = user_input or {}
290+
160291
return self.async_show_form(
161292
step_id="user",
162293
data_schema=vol.Schema(
163294
{
164-
vol.Required(CONF_HOST): str,
165-
vol.Optional(CONF_PORT, default=DEFAULT_PORT): int,
166-
vol.Optional(CONF_PASSKEY): str,
167-
vol.Optional(CONF_USERNAME): str,
168-
vol.Optional(CONF_PASSWORD): str,
295+
vol.Required(
296+
CONF_HOST, default=defaults.get(CONF_HOST, vol.UNDEFINED)
297+
): str,
298+
vol.Optional(
299+
CONF_PORT, default=defaults.get(CONF_PORT, DEFAULT_PORT)
300+
): int,
301+
vol.Optional(
302+
CONF_PASSKEY, default=defaults.get(CONF_PASSKEY, vol.UNDEFINED)
303+
): str,
304+
vol.Optional(
305+
CONF_USERNAME,
306+
default=defaults.get(CONF_USERNAME, vol.UNDEFINED),
307+
): str,
308+
vol.Optional(
309+
CONF_PASSWORD,
310+
default=defaults.get(CONF_PASSWORD, vol.UNDEFINED),
311+
): str,
169312
}
170313
),
171314
errors=errors or {},
@@ -186,7 +329,9 @@ def _async_create_entry(self) -> ConfigFlowResult:
186329
)
187330

188331
async def _get_bsblan_info(
189-
self, raise_on_progress: bool = True, is_discovery: bool = False
332+
self,
333+
raise_on_progress: bool = True,
334+
is_reauth: bool = False,
190335
) -> None:
191336
"""Get device information from a BSBLAN device."""
192337
config = BSBLANConfig(
@@ -209,11 +354,13 @@ async def _get_bsblan_info(
209354
format_mac(self.mac), raise_on_progress=raise_on_progress
210355
)
211356

212-
# Always allow updating host/port for both user and discovery flows
213-
# This ensures connectivity is maintained when devices change IP addresses
214-
self._abort_if_unique_id_configured(
215-
updates={
216-
CONF_HOST: self.host,
217-
CONF_PORT: self.port,
218-
}
219-
)
357+
# Skip unique_id configuration check during reauth to prevent "already_configured" abort
358+
if not is_reauth:
359+
# Always allow updating host/port for both user and discovery flows
360+
# This ensures connectivity is maintained when devices change IP addresses
361+
self._abort_if_unique_id_configured(
362+
updates={
363+
CONF_HOST: self.host,
364+
CONF_PORT: self.port,
365+
}
366+
)

homeassistant/components/bsblan/coordinator.py

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,19 @@
44
from datetime import timedelta
55
from random import randint
66

7-
from bsblan import BSBLAN, BSBLANConnectionError, HotWaterState, Sensor, State
7+
from bsblan import (
8+
BSBLAN,
9+
BSBLANAuthError,
10+
BSBLANConnectionError,
11+
HotWaterState,
12+
Sensor,
13+
State,
14+
)
815

916
from homeassistant.config_entries import ConfigEntry
1017
from homeassistant.const import CONF_HOST
1118
from homeassistant.core import HomeAssistant
19+
from homeassistant.exceptions import ConfigEntryAuthFailed
1220
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
1321

1422
from .const import DOMAIN, LOGGER, SCAN_INTERVAL
@@ -62,6 +70,10 @@ async def _async_update_data(self) -> BSBLanCoordinatorData:
6270
state = await self.client.state()
6371
sensor = await self.client.sensor()
6472
dhw = await self.client.hot_water_state()
73+
except BSBLANAuthError as err:
74+
raise ConfigEntryAuthFailed(
75+
"Authentication failed for BSB-Lan device"
76+
) from err
6577
except BSBLANConnectionError as err:
6678
host = self.config_entry.data[CONF_HOST] if self.config_entry else "unknown"
6779
raise UpdateFailed(

homeassistant/components/bsblan/strings.json

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,14 +33,25 @@
3333
"username": "[%key:component::bsblan::config::step::user::data_description::username%]",
3434
"password": "[%key:component::bsblan::config::step::user::data_description::password%]"
3535
}
36+
},
37+
"reauth_confirm": {
38+
"title": "[%key:common::config_flow::title::reauth%]",
39+
"description": "The BSB-Lan integration needs to re-authenticate with {name}",
40+
"data": {
41+
"passkey": "[%key:component::bsblan::config::step::user::data::passkey%]",
42+
"username": "[%key:common::config_flow::data::username%]",
43+
"password": "[%key:common::config_flow::data::password%]"
44+
}
3645
}
3746
},
3847
"error": {
39-
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]"
48+
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
49+
"invalid_auth": "[%key:common::config_flow::error::invalid_auth%]"
4050
},
4151
"abort": {
4252
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]",
43-
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]"
53+
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
54+
"reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]"
4455
}
4556
},
4657
"exceptions": {

0 commit comments

Comments
 (0)