1212from homeassistant .core import callback
1313from homeassistant .helpers .aiohttp_client import async_get_clientsession
1414from homeassistant .helpers .device_registry import format_mac
15+ from homeassistant .helpers .service_info .zeroconf import ZeroconfServiceInfo
1516
1617from .const import CONF_PASSKEY , DEFAULT_PORT , DOMAIN
1718
@@ -21,12 +22,15 @@ class BSBLANFlowHandler(ConfigFlow, domain=DOMAIN):
2122
2223 VERSION = 1
2324
24- host : str
25- port : int
26- mac : str
27- passkey : str | None = None
28- username : str | None = None
29- password : str | None = None
25+ def __init__ (self ) -> None :
26+ """Initialize BSBLan flow."""
27+ self .host : str | None = None
28+ self .port : int = DEFAULT_PORT
29+ self .mac : str | None = None
30+ self .passkey : str | None = None
31+ self .username : str | None = None
32+ self .password : str | None = None
33+ self ._auth_required = True
3034
3135 async def async_step_user (
3236 self , user_input : dict [str , Any ] | None = None
@@ -41,9 +45,111 @@ async def async_step_user(
4145 self .username = user_input .get (CONF_USERNAME )
4246 self .password = user_input .get (CONF_PASSWORD )
4347
48+ return await self ._validate_and_create ()
49+
50+ async def async_step_zeroconf (
51+ self , discovery_info : ZeroconfServiceInfo
52+ ) -> ConfigFlowResult :
53+ """Handle Zeroconf discovery."""
54+
55+ self .host = str (discovery_info .ip_address )
56+ self .port = discovery_info .port or DEFAULT_PORT
57+
58+ # Get MAC from properties
59+ self .mac = discovery_info .properties .get ("mac" )
60+
61+ # If MAC was found in zeroconf, use it immediately
62+ if self .mac :
63+ await self .async_set_unique_id (format_mac (self .mac ))
64+ self ._abort_if_unique_id_configured (
65+ updates = {
66+ CONF_HOST : self .host ,
67+ CONF_PORT : self .port ,
68+ }
69+ )
70+ else :
71+ # MAC not available from zeroconf - check for existing host/port first
72+ self ._async_abort_entries_match (
73+ {CONF_HOST : self .host , CONF_PORT : self .port }
74+ )
75+
76+ # Try to get device info without authentication to minimize discovery popup
77+ config = BSBLANConfig (host = self .host , port = self .port )
78+ session = async_get_clientsession (self .hass )
79+ bsblan = BSBLAN (config , session )
80+ try :
81+ device = await bsblan .device ()
82+ except BSBLANError :
83+ # Device requires authentication - proceed to discovery confirm
84+ self .mac = None
85+ else :
86+ self .mac = device .MAC
87+
88+ # Got MAC without auth - set unique ID and check for existing device
89+ await self .async_set_unique_id (format_mac (self .mac ))
90+ self ._abort_if_unique_id_configured (
91+ updates = {
92+ CONF_HOST : self .host ,
93+ CONF_PORT : self .port ,
94+ }
95+ )
96+ # No auth needed, so we can proceed to a confirmation step without fields
97+ self ._auth_required = False
98+
99+ # Proceed to get credentials
100+ self .context ["title_placeholders" ] = {"name" : f"BSBLAN { self .host } " }
101+ return await self .async_step_discovery_confirm ()
102+
103+ async def async_step_discovery_confirm (
104+ self , user_input : dict [str , Any ] | None = None
105+ ) -> ConfigFlowResult :
106+ """Handle getting credentials for discovered device."""
107+ if user_input is None :
108+ data_schema = vol .Schema (
109+ {
110+ vol .Optional (CONF_PASSKEY ): str ,
111+ vol .Optional (CONF_USERNAME ): str ,
112+ vol .Optional (CONF_PASSWORD ): str ,
113+ }
114+ )
115+ if not self ._auth_required :
116+ data_schema = vol .Schema ({})
117+
118+ return self .async_show_form (
119+ step_id = "discovery_confirm" ,
120+ data_schema = data_schema ,
121+ description_placeholders = {"host" : str (self .host )},
122+ )
123+
124+ if not self ._auth_required :
125+ return self ._async_create_entry ()
126+
127+ self .passkey = user_input .get (CONF_PASSKEY )
128+ self .username = user_input .get (CONF_USERNAME )
129+ self .password = user_input .get (CONF_PASSWORD )
130+
131+ return await self ._validate_and_create (is_discovery = True )
132+
133+ async def _validate_and_create (
134+ self , is_discovery : bool = False
135+ ) -> ConfigFlowResult :
136+ """Validate device connection and create entry."""
44137 try :
45- await self ._get_bsblan_info ()
138+ await self ._get_bsblan_info (is_discovery = is_discovery )
46139 except BSBLANError :
140+ if is_discovery :
141+ return self .async_show_form (
142+ step_id = "discovery_confirm" ,
143+ data_schema = vol .Schema (
144+ {
145+ vol .Optional (CONF_PASSKEY ): str ,
146+ vol .Optional (CONF_USERNAME ): str ,
147+ vol .Optional (CONF_PASSWORD ): str ,
148+ }
149+ ),
150+ errors = {"base" : "cannot_connect" },
151+ description_placeholders = {"host" : str (self .host )},
152+ )
47153 return self ._show_setup_form ({"base" : "cannot_connect" })
48154
49155 return self ._async_create_entry ()
@@ -67,6 +173,7 @@ def _show_setup_form(self, errors: dict | None = None) -> ConfigFlowResult:
67173
68174 @callback
69175 def _async_create_entry (self ) -> ConfigFlowResult :
176+ """Create the config entry."""
70177 return self .async_create_entry (
71178 title = format_mac (self .mac ),
72179 data = {
@@ -78,8 +185,10 @@ def _async_create_entry(self) -> ConfigFlowResult:
78185 },
79186 )
80187
81- async def _get_bsblan_info (self , raise_on_progress : bool = True ) -> None :
82- """Get device information from an BSBLAN device."""
188+ async def _get_bsblan_info (
189+ self , raise_on_progress : bool = True , is_discovery : bool = False
190+ ) -> None :
191+ """Get device information from a BSBLAN device."""
83192 config = BSBLANConfig (
84193 host = self .host ,
85194 passkey = self .passkey ,
@@ -90,11 +199,18 @@ async def _get_bsblan_info(self, raise_on_progress: bool = True) -> None:
90199 session = async_get_clientsession (self .hass )
91200 bsblan = BSBLAN (config , session )
92201 device = await bsblan .device ()
93- self .mac = device .MAC
94-
95- await self .async_set_unique_id (
96- format_mac (self .mac ), raise_on_progress = raise_on_progress
97- )
202+ retrieved_mac = device .MAC
203+
204+ # Handle unique ID assignment based on whether MAC was available from zeroconf
205+ if not self .mac :
206+ # MAC wasn't available from zeroconf, now we have it from API
207+ self .mac = retrieved_mac
208+ await self .async_set_unique_id (
209+ format_mac (self .mac ), raise_on_progress = raise_on_progress
210+ )
211+
212+ # Always allow updating host/port for both user and discovery flows
213+ # This ensures connectivity is maintained when devices change IP addresses
98214 self ._abort_if_unique_id_configured (
99215 updates = {
100216 CONF_HOST : self .host ,
0 commit comments