1 """Config flow for Universal Devices ISY/IoX integration."""
3 from __future__
import annotations
6 from collections.abc
import Mapping
9 from urllib.parse
import urlparse, urlunparse
11 from aiohttp
import CookieJar
12 from pyisy
import ISYConnectionError, ISYInvalidAuthError, ISYResponseParseError
13 from pyisy.configuration
import Configuration
14 from pyisy.connection
import Connection
15 import voluptuous
as vol
33 CONF_RESTORE_LIGHT_STATE,
36 CONF_VAR_SENSOR_STRING,
37 DEFAULT_IGNORE_STRING,
38 DEFAULT_RESTORE_LIGHT_STATE,
39 DEFAULT_SENSOR_STRING,
41 DEFAULT_VAR_SENSOR_STRING,
53 _LOGGER = logging.getLogger(__name__)
57 """Generate schema with defaults."""
60 vol.Required(CONF_HOST, default=schema_input.get(CONF_HOST,
"")): str,
61 vol.Required(CONF_USERNAME): str,
62 vol.Required(CONF_PASSWORD): str,
63 vol.Optional(CONF_TLS_VER, default=DEFAULT_TLS_VERSION): vol.In([1.1, 1.2]),
65 extra=vol.ALLOW_EXTRA,
69 async
def validate_input(hass: HomeAssistant, data: dict[str, Any]) -> dict[str, str]:
70 """Validate the user input allows us to connect.
72 Data has the keys from DATA_SCHEMA with values provided by the user.
74 user = data[CONF_USERNAME]
75 password = data[CONF_PASSWORD]
76 host = urlparse(data[CONF_HOST])
77 tls_version = data.get(CONF_TLS_VER)
79 if host.scheme == SCHEME_HTTP:
81 port = host.port
or HTTP_PORT
82 session = aiohttp_client.async_create_clientsession(
83 hass, verify_ssl=
False, cookie_jar=CookieJar(unsafe=
True)
85 elif host.scheme == SCHEME_HTTPS:
87 port = host.port
or HTTPS_PORT
88 session = aiohttp_client.async_get_clientsession(hass)
90 _LOGGER.error(
"The ISY/IoX host value in configuration is invalid")
94 isy_conn = Connection(
106 async
with asyncio.timeout(30):
107 isy_conf_xml = await isy_conn.test_connection()
108 except ISYInvalidAuthError
as error:
109 raise InvalidAuth
from error
110 except ISYConnectionError
as error:
111 raise CannotConnect
from error
114 isy_conf = Configuration(xml=isy_conf_xml)
115 except ISYResponseParseError
as error:
116 raise CannotConnect
from error
117 if not isy_conf
or ISY_CONF_NAME
not in isy_conf
or not isy_conf[ISY_CONF_NAME]:
122 "title": f
"{isy_conf[ISY_CONF_NAME]} ({host.hostname})",
123 ISY_CONF_UUID: isy_conf[ISY_CONF_UUID],
128 """Handle a config flow for Universal Devices ISY/IoX."""
133 """Initialize the ISY/IoX config flow."""
140 config_entry: ConfigEntry,
142 """Get the options flow for this handler."""
146 self, user_input: dict[str, Any] |
None =
None
147 ) -> ConfigFlowResult:
148 """Handle the initial step."""
150 info: dict[str, str] = {}
151 if user_input
is not None:
154 except CannotConnect:
155 errors[
"base"] =
"cannot_connect"
157 errors[
"base"] =
"invalid_host"
159 errors[CONF_PASSWORD] =
"invalid_auth"
161 _LOGGER.exception(
"Unexpected exception")
162 errors[
"base"] =
"unknown"
166 info[ISY_CONF_UUID], raise_on_progress=
False
178 self, isy_mac: str, ip_address: str, port: int |
None
180 """Abort and update the ip address on change."""
182 if not existing_entry:
184 if existing_entry.source == SOURCE_IGNORE:
186 parsed_url = urlparse(existing_entry.data[CONF_HOST])
187 if parsed_url.hostname != ip_address:
188 new_netloc = ip_address
190 new_netloc = f
"{ip_address}:{port}"
191 elif parsed_url.port:
192 new_netloc = f
"{ip_address}:{parsed_url.port}"
193 self.hass.config_entries.async_update_entry(
196 **existing_entry.data,
197 CONF_HOST: urlunparse(
212 self, discovery_info: dhcp.DhcpServiceInfo
213 ) -> ConfigFlowResult:
214 """Handle a discovered ISY/IoX device via dhcp."""
215 friendly_name = discovery_info.hostname
216 if friendly_name.startswith((
"polisy",
"eisy")):
217 url = f
"http://{discovery_info.ip}:8080"
219 url = f
"http://{discovery_info.ip}"
220 mac = discovery_info.macaddress
222 f
"{mac[0:2]}:{mac[2:4]}:{mac[4:6]}:{mac[6:8]}:{mac[8:10]}:{mac[10:12]}"
227 CONF_NAME: friendly_name,
231 self.context[
"title_placeholders"] = self.
discovered_confdiscovered_conf
235 self, discovery_info: ssdp.SsdpServiceInfo
236 ) -> ConfigFlowResult:
237 """Handle a discovered ISY/IoX Device."""
238 friendly_name = discovery_info.upnp[ssdp.ATTR_UPNP_FRIENDLY_NAME]
239 url = discovery_info.ssdp_location
240 assert isinstance(url, str)
241 parsed_url = urlparse(url)
242 mac = discovery_info.upnp[ssdp.ATTR_UPNP_UDN]
243 mac = mac.removeprefix(UDN_UUID_PREFIX)
244 url = url.removesuffix(ISY_URL_POSTFIX)
248 port = parsed_url.port
249 elif parsed_url.scheme == SCHEME_HTTPS:
252 assert isinstance(parsed_url.hostname, str)
256 CONF_NAME: friendly_name,
260 self.context[
"title_placeholders"] = self.
discovered_confdiscovered_conf
264 self, entry_data: Mapping[str, Any]
265 ) -> ConfigFlowResult:
271 self, user_input: dict[str, Any] |
None =
None
272 ) -> ConfigFlowResult:
273 """Handle reauth input."""
277 existing_data = existing_entry.data
278 if user_input
is not None:
281 CONF_USERNAME: user_input[CONF_USERNAME],
282 CONF_PASSWORD: user_input[CONF_PASSWORD],
286 except CannotConnect:
287 errors[
"base"] =
"cannot_connect"
289 errors[CONF_PASSWORD] =
"invalid_auth"
295 self.context[
"title_placeholders"] = {
296 CONF_NAME: existing_entry.title,
297 CONF_HOST: existing_data[CONF_HOST],
300 description_placeholders={CONF_HOST: existing_data[CONF_HOST]},
301 step_id=
"reauth_confirm",
302 data_schema=vol.Schema(
305 CONF_USERNAME, default=existing_data[CONF_USERNAME]
307 vol.Required(CONF_PASSWORD): str,
315 """Handle a option flow for ISY/IoX."""
318 self, user_input: dict[str, Any] |
None =
None
319 ) -> ConfigFlowResult:
320 """Handle options flow."""
321 if user_input
is not None:
325 restore_light_state = options.get(
326 CONF_RESTORE_LIGHT_STATE, DEFAULT_RESTORE_LIGHT_STATE
328 ignore_string = options.get(CONF_IGNORE_STRING, DEFAULT_IGNORE_STRING)
329 sensor_string = options.get(CONF_SENSOR_STRING, DEFAULT_SENSOR_STRING)
330 var_sensor_string = options.get(
331 CONF_VAR_SENSOR_STRING, DEFAULT_VAR_SENSOR_STRING
334 options_schema = vol.Schema(
336 vol.Optional(CONF_IGNORE_STRING, default=ignore_string): str,
337 vol.Optional(CONF_SENSOR_STRING, default=sensor_string): str,
338 vol.Optional(CONF_VAR_SENSOR_STRING, default=var_sensor_string): str,
340 CONF_RESTORE_LIGHT_STATE, default=restore_light_state
345 return self.
async_show_formasync_show_form(step_id=
"init", data_schema=options_schema)
349 """Error to indicate the host value is invalid."""
352 class CannotConnect(HomeAssistantError):
353 """Error to indicate we cannot connect."""
357 """Error to indicate there is invalid auth."""
ConfigFlowResult async_step_reauth_confirm(self, dict[str, Any]|None user_input=None)
ConfigFlowResult async_step_reauth(self, Mapping[str, Any] entry_data)
ConfigFlowResult async_step_dhcp(self, dhcp.DhcpServiceInfo discovery_info)
ConfigFlowResult async_step_user(self, dict[str, Any]|None user_input=None)
OptionsFlow async_get_options_flow(ConfigEntry config_entry)
None _async_set_unique_id_or_update(self, str isy_mac, str ip_address, int|None port)
ConfigFlowResult async_step_ssdp(self, ssdp.SsdpServiceInfo discovery_info)
ConfigFlowResult async_step_init(self, dict[str, Any]|None user_input=None)
None _abort_if_unique_id_configured(self, dict[str, Any]|None updates=None, bool reload_on_update=True, *str error="already_configured")
ConfigEntry|None async_set_unique_id(self, str|None unique_id=None, *bool raise_on_progress=True)
ConfigFlowResult async_create_entry(self, *str title, Mapping[str, Any] data, str|None description=None, Mapping[str, str]|None description_placeholders=None, Mapping[str, Any]|None options=None)
ConfigFlowResult async_update_reload_and_abort(self, ConfigEntry entry, *str|None|UndefinedType unique_id=UNDEFINED, str|UndefinedType title=UNDEFINED, Mapping[str, Any]|UndefinedType data=UNDEFINED, Mapping[str, Any]|UndefinedType data_updates=UNDEFINED, Mapping[str, Any]|UndefinedType options=UNDEFINED, str|UndefinedType reason=UNDEFINED, bool reload_even_if_entry_is_unchanged=True)
ConfigFlowResult async_step_user(self, dict[str, Any]|None user_input=None)
ConfigFlowResult async_show_form(self, *str|None step_id=None, vol.Schema|None data_schema=None, dict[str, str]|None errors=None, Mapping[str, str]|None description_placeholders=None, bool|None last_step=None, str|None preview=None)
ConfigEntry config_entry(self)
None config_entry(self, ConfigEntry value)
_FlowResultT async_show_form(self, *str|None step_id=None, vol.Schema|None data_schema=None, dict[str, str]|None errors=None, Mapping[str, str]|None description_placeholders=None, bool|None last_step=None, str|None preview=None)
_FlowResultT async_create_entry(self, *str|None title=None, Mapping[str, Any] data, str|None description=None, Mapping[str, str]|None description_placeholders=None)
vol.Schema _data_schema(dict[str, str] schema_input)
dict[str, str] validate_input(HomeAssistant hass, dict[str, Any] data)