1 """Config flow for Broadlink devices."""
3 from collections.abc
import Mapping
5 from functools
import partial
10 import broadlink
as blk
11 from broadlink.exceptions
import (
16 import voluptuous
as vol
29 from .const
import DEFAULT_PORT, DEFAULT_TIMEOUT, DEVICE_TYPES, DOMAIN
30 from .helpers
import format_mac
32 _LOGGER = logging.getLogger(__name__)
36 """Handle a Broadlink config flow."""
43 self, device: blk.Device, raise_on_progress: bool =
True
45 """Define a device for the config flow."""
46 if device.type
not in DEVICE_TYPES:
49 "Unsupported device: %s. If it worked before, please open "
50 "an issue at https://github.com/home-assistant/core/issues"
57 device.mac.hex(), raise_on_progress=raise_on_progress
61 self.context[
"title_placeholders"] = {
63 "model": device.model,
64 "host": device.host[0],
68 self, discovery_info: dhcp.DhcpServiceInfo
69 ) -> ConfigFlowResult:
70 """Handle dhcp discovery."""
71 host = discovery_info.ip
72 unique_id = discovery_info.macaddress.lower().replace(
":",
"")
77 device = await self.hass.async_add_executor_job(blk.hello, host)
79 except NetworkTimeoutError:
82 except OSError
as err:
83 if err.errno == errno.ENETUNREACH:
87 if device.type
not in DEVICE_TYPES:
94 self, user_input: dict[str, Any] |
None =
None
95 ) -> ConfigFlowResult:
96 """Handle a flow initiated by the user."""
99 if user_input
is not None:
100 host = user_input[CONF_HOST]
101 timeout = user_input.get(CONF_TIMEOUT, DEFAULT_TIMEOUT)
104 hello = partial(blk.hello, host, timeout=timeout)
105 device = await self.hass.async_add_executor_job(hello)
107 except NetworkTimeoutError:
108 errors[
"base"] =
"cannot_connect"
109 err_msg =
"Device not found"
111 except OSError
as err:
112 if err.errno
in {errno.EINVAL, socket.EAI_NONAME}:
113 errors[
"base"] =
"invalid_host"
114 err_msg =
"Invalid hostname or IP address"
115 elif err.errno == errno.ENETUNREACH:
116 errors[
"base"] =
"cannot_connect"
119 errors[
"base"] =
"unknown"
123 device.timeout = timeout
128 updates={CONF_HOST: device.host[0], CONF_TIMEOUT: timeout}
132 if device.mac == self.
devicedevice.mac:
133 await self.
async_set_deviceasync_set_device(device, raise_on_progress=
False)
136 errors[
"base"] =
"invalid_host"
138 "This is not the device you are looking for. The MAC "
139 f
"address must be {format_mac(self.device.mac)}"
142 _LOGGER.error(
"Failed to connect to the device at %s: %s", host, err_msg)
148 vol.Required(CONF_HOST): str,
149 vol.Optional(CONF_TIMEOUT, default=DEFAULT_TIMEOUT): cv.positive_int,
153 data_schema=vol.Schema(data_schema),
158 """Authenticate to the device."""
159 device = self.
devicedevice
160 errors: dict[str, str] = {}
163 await self.hass.async_add_executor_job(device.auth)
165 except AuthenticationError:
166 errors[
"base"] =
"invalid_auth"
170 except NetworkTimeoutError
as err:
171 errors[
"base"] =
"cannot_connect"
174 except BroadlinkException
as err:
175 errors[
"base"] =
"unknown"
178 except OSError
as err:
179 if err.errno == errno.ENETUNREACH:
180 errors[
"base"] =
"cannot_connect"
183 errors[
"base"] =
"unknown"
191 "%s (%s at %s) is ready to be configured. Click "
192 "Configuration in the sidebar, click Integrations and "
193 "click Configure on the device to complete the setup"
206 "Failed to authenticate to the device at %s: %s", device.host[0], err_msg
212 user_input: dict[str, Any] |
None =
None,
213 errors: dict[str, str] |
None =
None,
214 ) -> ConfigFlowResult:
215 """Guide the user to unlock the device manually.
217 We are unable to authenticate because the device is locked.
218 The user needs to open the Broadlink app and unlock the device.
220 device = self.
devicedevice
222 if user_input
is None:
226 description_placeholders={
228 "model": device.model,
229 "host": device.host[0],
234 {CONF_HOST: device.host[0], CONF_TIMEOUT: device.timeout}
238 self, user_input: dict[str, Any] |
None =
None
239 ) -> ConfigFlowResult:
240 """Unlock the device.
242 The authentication succeeded, but the device is locked.
243 We can offer an unlock to prevent authorization errors.
245 device = self.
devicedevice
248 if user_input
is None:
251 elif user_input[
"unlock"]:
253 await self.hass.async_add_executor_job(device.set_lock,
False)
255 except NetworkTimeoutError
as err:
256 errors[
"base"] =
"cannot_connect"
259 except BroadlinkException
as err:
260 errors[
"base"] =
"unknown"
263 except OSError
as err:
264 if err.errno == errno.ENETUNREACH:
265 errors[
"base"] =
"cannot_connect"
268 errors[
"base"] =
"unknown"
275 "Failed to unlock the device at %s: %s", device.host[0], err_msg
281 data_schema = {vol.Required(
"unlock", default=
False): bool}
285 data_schema=vol.Schema(data_schema),
286 description_placeholders={
288 "model": device.model,
289 "host": device.host[0],
294 self, user_input: dict[str, Any] |
None =
None
295 ) -> ConfigFlowResult:
296 """Choose a name for the device and create config entry."""
297 device = self.
devicedevice
298 errors: dict[str, str] = {}
302 updates={CONF_HOST: device.host[0], CONF_TIMEOUT: device.timeout}
305 if user_input
is not None:
307 title=user_input[CONF_NAME],
309 CONF_HOST: device.host[0],
310 CONF_MAC: device.mac.hex(),
311 CONF_TYPE: device.devtype,
312 CONF_TIMEOUT: device.timeout,
316 data_schema = {vol.Required(CONF_NAME, default=device.name): str}
318 step_id=
"finish", data_schema=vol.Schema(data_schema), errors=errors
322 """Import a device."""
327 self, entry_data: Mapping[str, Any]
328 ) -> ConfigFlowResult:
329 """Reauthenticate to the device."""
330 device = blk.gendevice(
331 entry_data[CONF_TYPE],
332 (entry_data[CONF_HOST], DEFAULT_PORT),
333 bytes.fromhex(entry_data[CONF_MAC]),
334 name=entry_data[CONF_NAME],
336 device.timeout = entry_data[CONF_TIMEOUT]
None async_set_device(self, blk.Device device, bool raise_on_progress=True)
ConfigFlowResult async_step_import(self, dict[str, Any] import_data)
ConfigFlowResult async_step_dhcp(self, dhcp.DhcpServiceInfo discovery_info)
ConfigFlowResult async_step_auth(self)
ConfigFlowResult async_step_reset(self, dict[str, Any]|None user_input=None, dict[str, str]|None errors=None)
ConfigFlowResult async_step_unlock(self, dict[str, Any]|None user_input=None)
ConfigFlowResult async_step_user(self, dict[str, Any]|None user_input=None)
ConfigFlowResult async_step_reauth(self, Mapping[str, Any] entry_data)
ConfigFlowResult async_step_finish(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_step_user(self, dict[str, Any]|None user_input=None)
ConfigFlowResult async_abort(self, *str reason, Mapping[str, str]|None description_placeholders=None)
None _async_abort_entries_match(self, dict[str, Any]|None match_dict=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)
_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)
_FlowResultT async_abort(self, *str reason, Mapping[str, str]|None description_placeholders=None)