22
33from __future__ import annotations
44
5+ from collections .abc import Mapping
56from typing import Any
67
7- from bsblan import BSBLAN , BSBLANConfig , BSBLANError
8+ from bsblan import BSBLAN , BSBLANAuthError , BSBLANConfig , BSBLANError
89import voluptuous as vol
910
1011from 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+ )
0 commit comments