1 """Config flow for Switchbot."""
3 from __future__
import annotations
8 from switchbot
import (
9 SwitchbotAccountConnectionError,
10 SwitchBotAdvertisement,
12 SwitchbotAuthenticationError,
14 parse_advertisement_data,
16 import voluptuous
as vol
19 BluetoothServiceInfoBleak,
20 async_discovered_service_info,
43 CONNECTABLE_SUPPORTED_MODEL_TYPES,
44 DEFAULT_LOCK_NIGHTLATCH,
47 NON_CONNECTABLE_SUPPORTED_MODEL_TYPES,
48 SUPPORTED_LOCK_MODELS,
49 SUPPORTED_MODEL_TYPES,
53 _LOGGER = logging.getLogger(__name__)
57 """Format the unique ID for a switchbot."""
58 return address.replace(
":",
"").lower()
62 """Convert a Bluetooth address to a short address."""
63 results = address.replace(
"-",
":").split(
":")
64 return f
"{results[-2].upper()}{results[-1].upper()}"[-4:]
68 """Get the name from a discovery."""
69 return f
'{discovery.data["modelFriendlyName"]} {short_address(discovery.address)}'
73 """Handle a config flow for Switchbot."""
80 config_entry: ConfigEntry,
81 ) -> SwitchbotOptionsFlowHandler:
82 """Get the options flow for this handler."""
86 """Initialize the config flow."""
87 self.
_discovered_adv_discovered_adv: SwitchBotAdvertisement |
None =
None
88 self._discovered_advs: dict[str, SwitchBotAdvertisement] = {}
91 self, discovery_info: BluetoothServiceInfoBleak
92 ) -> ConfigFlowResult:
93 """Handle the bluetooth discovery step."""
94 _LOGGER.debug(
"Discovered bluetooth device: %s", discovery_info.as_dict())
97 parsed = parse_advertisement_data(
98 discovery_info.device, discovery_info.advertisement
100 if not parsed
or parsed.data.get(
"modelName")
not in SUPPORTED_MODEL_TYPES:
102 model_name = parsed.data.get(
"modelName")
104 not discovery_info.connectable
105 and model_name
in CONNECTABLE_SUPPORTED_MODEL_TYPES
111 self.context[
"title_placeholders"] = {
112 "name": data[
"modelFriendlyName"],
115 if model_name
in SUPPORTED_LOCK_MODELS:
122 self, user_input: dict[str, Any]
123 ) -> ConfigFlowResult:
124 """Create an entry from a discovery."""
128 model_name = discovery.data[
"modelName"]
133 CONF_ADDRESS: discovery.address,
134 CONF_SENSOR_TYPE:
str(SUPPORTED_MODEL_TYPES[model_name]),
139 self, user_input: dict[str, Any] |
None =
None
140 ) -> ConfigFlowResult:
141 """Confirm a single device."""
143 if user_input
is not None:
149 data_schema=vol.Schema({}),
150 description_placeholders={
156 self, user_input: dict[str, Any] |
None =
None
157 ) -> ConfigFlowResult:
158 """Handle the password step."""
160 if user_input
is not None:
168 data_schema=vol.Schema({vol.Required(CONF_PASSWORD): str}),
169 description_placeholders={
175 self, user_input: dict[str, Any] |
None =
None
176 ) -> ConfigFlowResult:
177 """Handle the SwitchBot API auth step."""
180 description_placeholders = {}
181 if user_input
is not None:
183 key_details = await SwitchbotLock.async_retrieve_encryption_key(
186 user_input[CONF_USERNAME],
187 user_input[CONF_PASSWORD],
189 except (SwitchbotApiError, SwitchbotAccountConnectionError)
as ex:
191 "Failed to connect to SwitchBot API: %s", ex, exc_info=
True
194 "api_error", description_placeholders={
"error_detail":
str(ex)}
196 except SwitchbotAuthenticationError
as ex:
197 _LOGGER.debug(
"Authentication failed: %s", ex, exc_info=
True)
198 errors = {
"base":
"auth_failed"}
199 description_placeholders = {
"error_detail":
str(ex)}
203 user_input = user_input
or {}
207 data_schema=vol.Schema(
210 CONF_USERNAME, default=user_input.get(CONF_USERNAME)
212 vol.Required(CONF_PASSWORD): str,
215 description_placeholders={
217 **description_placeholders,
222 self, user_input: dict[str, Any] |
None =
None
223 ) -> ConfigFlowResult:
224 """Handle the SwitchBot API chose method step."""
228 step_id=
"lock_choose_method",
229 menu_options=[
"lock_auth",
"lock_key"],
230 description_placeholders={
236 self, user_input: dict[str, Any] |
None =
None
237 ) -> ConfigFlowResult:
238 """Handle the encryption key step."""
241 if user_input
is not None:
242 if not await SwitchbotLock.verify_encryption_key(
244 user_input[CONF_KEY_ID],
245 user_input[CONF_ENCRYPTION_KEY],
249 "base":
"encryption_key_invalid",
257 data_schema=vol.Schema(
259 vol.Required(CONF_KEY_ID): str,
260 vol.Required(CONF_ENCRYPTION_KEY): str,
263 description_placeholders={
271 for connectable
in (
True,
False):
273 address = discovery_info.address
276 or address
in self._discovered_advs
279 parsed = parse_advertisement_data(
280 discovery_info.device, discovery_info.advertisement
284 model_name = parsed.data.get(
"modelName")
286 discovery_info.connectable
287 and model_name
in CONNECTABLE_SUPPORTED_MODEL_TYPES
288 )
or model_name
in NON_CONNECTABLE_SUPPORTED_MODEL_TYPES:
289 self._discovered_advs[address] = parsed
291 if not self._discovered_advs:
295 """Set the device to work with."""
297 address = discovery.address
304 self, user_input: dict[str, Any] |
None =
None
305 ) -> ConfigFlowResult:
306 """Handle the user step to pick discovered device."""
307 errors: dict[str, str] = {}
308 device_adv: SwitchBotAdvertisement |
None =
None
309 if user_input
is not None:
310 device_adv = self._discovered_advs[user_input[CONF_ADDRESS]]
312 if device_adv.data.get(
"modelName")
in SUPPORTED_LOCK_MODELS:
314 if device_adv.data[
"isEncrypted"]:
319 if len(self._discovered_advs) == 1:
322 device_adv =
list(self._discovered_advs.values())[0]
324 if device_adv.data.get(
"modelName")
in SUPPORTED_LOCK_MODELS:
326 if device_adv.data[
"isEncrypted"]:
332 data_schema=vol.Schema(
334 vol.Required(CONF_ADDRESS): vol.In(
337 for address, parsed
in self._discovered_advs.items()
347 """Handle Switchbot options."""
350 self, user_input: dict[str, Any] |
None =
None
351 ) -> ConfigFlowResult:
352 """Manage Switchbot options."""
353 if user_input
is not None:
357 options: dict[vol.Optional, Any] = {
361 CONF_RETRY_COUNT, DEFAULT_RETRY_COUNT
369 CONF_LOCK_NIGHTLATCH,
371 CONF_LOCK_NIGHTLATCH, DEFAULT_LOCK_NIGHTLATCH
377 return self.
async_show_formasync_show_form(step_id=
"init", data_schema=vol.Schema(options))
ConfigFlowResult async_step_lock_auth(self, dict[str, Any]|None user_input=None)
None _async_discover_devices(self)
SwitchbotOptionsFlowHandler async_get_options_flow(ConfigEntry config_entry)
ConfigFlowResult _async_create_entry_from_discovery(self, dict[str, Any] user_input)
ConfigFlowResult async_step_password(self, dict[str, Any]|None user_input=None)
ConfigFlowResult async_step_confirm(self, dict[str, Any]|None user_input=None)
ConfigFlowResult async_step_lock_key(self, dict[str, Any]|None user_input=None)
ConfigFlowResult async_step_lock_choose_method(self, dict[str, Any]|None user_input=None)
ConfigFlowResult async_step_user(self, dict[str, Any]|None user_input=None)
None _async_set_device(self, SwitchBotAdvertisement discovery)
ConfigFlowResult async_step_bluetooth(self, BluetoothServiceInfoBleak 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")
None _set_confirm_only(self)
set[str|None] _async_current_ids(self, bool include_ignore=True)
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_abort(self, *str reason, Mapping[str, str]|None description_placeholders=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_show_menu(self, *str|None step_id=None, Container[str] menu_options, Mapping[str, str]|None description_placeholders=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)
Iterable[BluetoothServiceInfoBleak] async_discovered_service_info(HomeAssistant hass, bool connectable=True)
str name_from_discovery(SwitchBotAdvertisement discovery)
str short_address(str address)
str format_unique_id(str address)
aiohttp.ClientSession async_get_clientsession(HomeAssistant hass, bool verify_ssl=True, socket.AddressFamily family=socket.AF_UNSPEC, ssl_util.SSLCipherList ssl_cipher=ssl_util.SSLCipherList.PYTHON_DEFAULT)