22
33from __future__ import annotations
44
5- import contextlib
65import logging
7- import socket
86from typing import Any
97
10- from enum import StrEnum
118import voluptuous as vol
129from zcc import (
1310 ControlPoint ,
11+ ControlPointCannotConnectError ,
12+ ControlPointConnectionRefusedError ,
1413 ControlPointDescription ,
1514 ControlPointDiscoveryService ,
1615 ControlPointError ,
16+ ControlPointInvalidHostError ,
17+ ControlPointTimeoutError ,
1718)
1819
1920from homeassistant .config_entries import ConfigFlow , ConfigFlowResult
2627 SelectSelectorMode ,
2728)
2829
29- from . import async_connect_to_controller
30- from .const import DEFAULT_PORT , DOMAIN , TITLE
30+ from .const import DOMAIN
3131
3232_LOGGER = logging .getLogger (__name__ )
33-
34- STEP_USER_DATA_SCHEMA = vol .Schema (
33+ DEFAULT_PORT = 5003
34+ STEP_MANUAL_DATA_SCHEMA = vol .Schema (
3535 {
36- vol .Required (CONF_HOST , default = "" ): str ,
36+ vol .Required (CONF_HOST ): str ,
3737 vol .Required (CONF_PORT , default = DEFAULT_PORT ): int ,
3838 }
3939)
4040
4141SELECTED_HOST_AND_PORT = "selected_host_and_port"
4242
4343
44- class ZimiConfigErrors (StrEnum ):
45- """ZimiConfig errors."""
46-
47- ALREADY_CONFIGURED = "already_configured"
48- CANNOT_CONNECT = "cannot_connect"
49- CONNECTION_REFUSED = "connection_refused"
50- DISCOVERY_FAILURE = "discovery_failure"
51- INVALID_HOST = "invalid_host"
52- INVALID_PORT = "invalid_port"
53- TIMEOUT = "timeout"
54- UNKNOWN = "unknown"
55-
56-
5744class ZimiConfigFlow (ConfigFlow , domain = DOMAIN ):
5845 """Handle a config flow for zcc."""
5946
6047 api : ControlPoint = None
6148 api_descriptions : list [ControlPointDescription ]
6249 data : dict [str , Any ]
6350
64- def __del__ (self ):
65- """Disconnect from ZCC."""
66- if self .api :
67- self .api .disconnect ()
68-
6951 async def async_step_user (
7052 self , user_input : dict [str , Any ] | None = None
7153 ) -> ConfigFlowResult :
@@ -75,13 +57,14 @@ async def async_step_user(
7557
7658 try :
7759 self .api_descriptions = await ControlPointDiscoveryService ().discovers ()
78- except ControlPointError as e :
79- _LOGGER . error ( e )
60+ except ControlPointError :
61+ # ControlPointError is expected if no zcc are found on LAN
8062 return await self .async_step_manual ()
8163
8264 if len (self .api_descriptions ) == 1 :
8365 self .data [CONF_HOST ] = self .api_descriptions [0 ].host
8466 self .data [CONF_PORT ] = self .api_descriptions [0 ].port
67+ await self .check_connection (self .data [CONF_HOST ], self .data [CONF_PORT ])
8568 return await self .create_entry ()
8669
8770 return await self .async_step_selection ()
@@ -91,14 +74,16 @@ async def async_step_selection(
9174 ) -> ConfigFlowResult :
9275 """Handle selection of zcc to configure if multiple are discovered."""
9376
94- errors : dict [str , str ] = {}
77+ errors : dict [str , str ] | None = {}
9578
9679 if user_input is not None :
97- self .data [CONF_HOST ] = user_input [SELECTED_HOST_AND_PORT ].split (":" )[
98- 0 ]
99- self .data [CONF_PORT ] = int (
100- user_input [SELECTED_HOST_AND_PORT ].split (":" )[1 ])
101- return await self .create_entry ()
80+ self .data [CONF_HOST ] = user_input [SELECTED_HOST_AND_PORT ].split (":" )[0 ]
81+ self .data [CONF_PORT ] = int (user_input [SELECTED_HOST_AND_PORT ].split (":" )[1 ])
82+ errors = await self .check_connection (
83+ self .data [CONF_HOST ], self .data [CONF_PORT ]
84+ )
85+ if not errors :
86+ return await self .create_entry ()
10287
10388 available_options = [
10489 SelectOptionDict (
@@ -130,12 +115,12 @@ async def async_step_manual(
130115 ) -> ConfigFlowResult :
131116 """Handle manual configuration step if needed."""
132117
133- errors : dict [str , str ] = {}
118+ errors : dict [str , str ] | None = {}
134119
135120 if user_input is not None :
136121 self .data = {** self .data , ** user_input }
137122
138- errors = await self .validate_connection (
123+ errors = await self .check_connection (
139124 self .data [CONF_HOST ], self .data [CONF_PORT ]
140125 )
141126
@@ -145,73 +130,43 @@ async def async_step_manual(
145130 return self .async_show_form (
146131 step_id = "manual" ,
147132 data_schema = self .add_suggested_values_to_schema (
148- STEP_USER_DATA_SCHEMA , self .data
133+ STEP_MANUAL_DATA_SCHEMA , self .data
149134 ),
150135 errors = errors ,
151136 )
152137
153- async def create_entry (self ) -> ConfigFlowResult :
154- """Create entry for zcc."""
138+ async def check_connection (self , host : str , port : int ) -> dict [ str , str ] | None :
139+ """Check connection to zcc.
155140
156- if not self .api :
157- with contextlib .suppress (ControlPointError ):
158- self .api = await async_connect_to_controller (
159- self .data [CONF_HOST ], self .data [CONF_PORT ], fast = False
160- )
141+ Stores mac and returns None if successful, otherwise returns error message.
142+ """
161143
162- if self .api and self .api .ready :
163- self .data [CONF_MAC ] = format_mac (self .api .mac )
164- self .api .disconnect ()
165- await self .async_set_unique_id (self .data [CONF_MAC ])
166- self ._abort_if_unique_id_configured ()
167- return self .async_create_entry (
168- title = f"{ TITLE } ({ self .data [CONF_HOST ]} :{ self .data [CONF_PORT ]} )" ,
169- data = self .data ,
144+ try :
145+ result = await ControlPointDiscoveryService ().validate_connection (
146+ self .data [CONF_HOST ], self .data [CONF_PORT ]
170147 )
148+ except ControlPointInvalidHostError :
149+ return {"base" : "invalid_host" }
150+ except ControlPointConnectionRefusedError :
151+ return {"base" : "connection_refused" }
152+ except ControlPointCannotConnectError :
153+ return {"base" : "cannot_connect" }
154+ except ControlPointTimeoutError :
155+ return {"base" : "timeout" }
156+ except Exception :
157+ _LOGGER .exception ("Unexpected error" )
158+ return {"base" : "unknown" }
159+
160+ self .data [CONF_MAC ] = format_mac (result .mac )
161+
162+ return None
171163
172- return self .async_abort (reason = "cannot_connect" )
173-
174- async def validate_connection (self , host : str , port : int ) -> dict [str , str ]:
175- """Check for errors with configuration.
176-
177- 1. Check connectivity to configured host and port; and
178- 2. Connect to ZCC to get mac address and store in self.data
179-
180- Return error dictionary upon failure.
181- """
164+ async def create_entry (self ) -> ConfigFlowResult :
165+ """Create entry for zcc."""
182166
183- try :
184- hostbyname = socket .gethostbyname (host )
185- except socket .gaierror as e :
186- _LOGGER .error (e )
187- return {"base" : ZimiConfigErrors .INVALID_HOST }
188- if hostbyname :
189- s = socket .socket (socket .AF_INET , socket .SOCK_STREAM )
190- s .settimeout (10 )
191- try :
192- s .connect ((host , port ))
193- s .close ()
194- except ConnectionRefusedError as e :
195- _LOGGER .error (e )
196- return {"base" : ZimiConfigErrors .CONNECTION_REFUSED }
197- except TimeoutError as e :
198- _LOGGER .error (e )
199- return {"base" : ZimiConfigErrors .TIMEOUT }
200- except socket .gaierror as e :
201- _LOGGER .error (e )
202- return {"base" : ZimiConfigErrors .CANNOT_CONNECT }
203- else :
204- return {"base" : ZimiConfigErrors .INVALID_HOST }
205-
206- if not self .api or not self .api .ready :
207- try :
208- self .api = await async_connect_to_controller (host , port , fast = True )
209- except ControlPointError as e :
210- _LOGGER .error (e )
211- return {"base" : ZimiConfigErrors .CANNOT_CONNECT }
212-
213- self .data [CONF_MAC ] = format_mac (self .api .mac )
214- self .api .disconnect ()
215- self .api = None
216-
217- return {}
167+ await self .async_set_unique_id (self .data [CONF_MAC ])
168+ self ._abort_if_unique_id_configured ()
169+ return self .async_create_entry (
170+ title = f"ZIMI Controller ({ self .data [CONF_HOST ]} :{ self .data [CONF_PORT ]} )" ,
171+ data = self .data ,
172+ )
0 commit comments