1 """Config flow for Shelly integration."""
3 from __future__
import annotations
5 from collections.abc
import Mapping
6 from typing
import Any, Final
8 from aioshelly.block_device
import BlockDevice
9 from aioshelly.common
import ConnectionOptions, get_info
10 from aioshelly.const
import BLOCK_GENERATIONS, DEFAULT_HTTP_PORT, RPC_GENERATIONS
11 from aioshelly.exceptions
import (
12 CustomPortNotSupported,
13 DeviceConnectionError,
15 MacAddressMismatchError,
17 from aioshelly.rpc_device
import RpcDevice
18 import voluptuous
as vol
39 CONF_BLE_SCANNER_MODE,
47 from .coordinator
import async_reconnect_soon
49 get_block_device_sleep_period,
56 get_rpc_device_wakeup_period,
58 mac_address_from_name,
61 CONFIG_SCHEMA: Final = vol.Schema(
63 vol.Required(CONF_HOST): str,
64 vol.Required(CONF_PORT, default=DEFAULT_HTTP_PORT): vol.Coerce(int),
69 BLE_SCANNER_OPTIONS = [
70 BLEScannerMode.DISABLED,
71 BLEScannerMode.ACTIVE,
72 BLEScannerMode.PASSIVE,
75 INTERNAL_WIFI_AP_IP =
"192.168.33.1"
85 """Validate the user input allows us to connect.
87 Data has the keys from CONFIG_SCHEMA with values provided by the user.
89 options = ConnectionOptions(
91 username=data.get(CONF_USERNAME),
92 password=data.get(CONF_PASSWORD),
93 device_mac=info[CONF_MAC],
99 if gen
in RPC_GENERATIONS:
101 rpc_device = await RpcDevice.create(
107 await rpc_device.initialize()
110 await rpc_device.shutdown()
113 "title": rpc_device.name,
114 CONF_SLEEP_PERIOD: sleep_period,
115 "model": rpc_device.shelly.get(
"model"),
121 block_device = await BlockDevice.create(
127 await block_device.initialize()
130 await block_device.shutdown()
133 "title": block_device.name,
134 CONF_SLEEP_PERIOD: sleep_period,
135 "model": block_device.model,
141 """Handle a config flow for Shelly."""
147 port: int = DEFAULT_HTTP_PORT
148 info: dict[str, Any] = {}
149 device_info: dict[str, Any] = {}
152 self, user_input: dict[str, Any] |
None =
None
153 ) -> ConfigFlowResult:
154 """Handle the initial step."""
155 errors: dict[str, str] = {}
156 if user_input
is not None:
157 host = user_input[CONF_HOST]
158 port = user_input[CONF_PORT]
160 self.
infoinfo = await self._async_get_info(host, port)
161 except DeviceConnectionError:
162 errors[
"base"] =
"cannot_connect"
164 LOGGER.exception(
"Unexpected exception")
165 errors[
"base"] =
"unknown"
172 return await self.async_step_credentials()
176 self.hass, host, port, self.
infoinfo, {}
178 except DeviceConnectionError:
179 errors[
"base"] =
"cannot_connect"
180 except MacAddressMismatchError:
181 errors[
"base"] =
"mac_address_mismatch"
182 except CustomPortNotSupported:
183 errors[
"base"] =
"custom_port_not_supported"
185 LOGGER.exception(
"Unexpected exception")
186 errors[
"base"] =
"unknown"
188 if device_info[
"model"]:
190 title=device_info[
"title"],
192 CONF_HOST: user_input[CONF_HOST],
193 CONF_PORT: user_input[CONF_PORT],
194 CONF_SLEEP_PERIOD: device_info[CONF_SLEEP_PERIOD],
195 "model": device_info[
"model"],
196 CONF_GEN: device_info[CONF_GEN],
199 errors[
"base"] =
"firmware_not_fully_provisioned"
202 step_id=
"user", data_schema=CONFIG_SCHEMA, errors=errors
205 async
def async_step_credentials(
206 self, user_input: dict[str, Any] |
None =
None
207 ) -> ConfigFlowResult:
208 """Handle the credentials step."""
209 errors: dict[str, str] = {}
210 if user_input
is not None:
212 user_input[CONF_USERNAME] =
"admin"
215 self.hass, self.
hosthost, self.
portport, self.
infoinfo, user_input
217 except InvalidAuthError:
218 errors[
"base"] =
"invalid_auth"
219 except DeviceConnectionError:
220 errors[
"base"] =
"cannot_connect"
221 except MacAddressMismatchError:
222 errors[
"base"] =
"mac_address_mismatch"
224 LOGGER.exception(
"Unexpected exception")
225 errors[
"base"] =
"unknown"
227 if device_info[
"model"]:
229 title=device_info[
"title"],
232 CONF_HOST: self.
hosthost,
233 CONF_PORT: self.
portport,
234 CONF_SLEEP_PERIOD: device_info[CONF_SLEEP_PERIOD],
235 "model": device_info[
"model"],
236 CONF_GEN: device_info[CONF_GEN],
239 errors[
"base"] =
"firmware_not_fully_provisioned"
246 CONF_PASSWORD, default=user_input.get(CONF_PASSWORD,
"")
252 CONF_USERNAME, default=user_input.get(CONF_USERNAME,
"")
255 CONF_PASSWORD, default=user_input.get(CONF_PASSWORD,
"")
260 step_id=
"credentials", data_schema=vol.Schema(schema), errors=errors
263 async
def _async_discovered_mac(self, mac: str, host: str) ->
None:
264 """Abort and reconnect soon if the device with the mac address is already configured."""
267 )
and current_entry.data.get(CONF_HOST) == host:
268 LOGGER.debug(
"async_reconnect_soon: host: %s, mac: %s", host, mac)
270 if host == INTERNAL_WIFI_AP_IP:
283 self, discovery_info: ZeroconfServiceInfo
284 ) -> ConfigFlowResult:
285 """Handle zeroconf discovery."""
286 if discovery_info.ip_address.version == 6:
288 host = discovery_info.host
293 await self._async_discovered_mac(mac, host)
298 self.
infoinfo = await self._async_get_info(host, DEFAULT_HTTP_PORT)
299 except DeviceConnectionError:
305 await self._async_discovered_mac(self.
infoinfo[CONF_MAC], host)
310 "title_placeholders": {
"name": discovery_info.name.split(
".")[0]},
311 "configuration_url": f
"http://{discovery_info.host}",
316 return await self.async_step_credentials()
320 self.hass, self.
hosthost, self.
portport, self.
infoinfo, {}
322 except DeviceConnectionError:
325 return await self.async_step_confirm_discovery()
327 async
def async_step_confirm_discovery(
328 self, user_input: dict[str, Any] |
None =
None
329 ) -> ConfigFlowResult:
330 """Handle discovery confirm."""
331 errors: dict[str, str] = {}
334 errors[
"base"] =
"firmware_not_fully_provisioned"
338 if user_input
is not None:
342 "host": self.
hosthost,
343 CONF_SLEEP_PERIOD: self.
device_infodevice_info[CONF_SLEEP_PERIOD],
351 step_id=
"confirm_discovery",
352 description_placeholders={
354 "host": self.
hosthost,
359 async
def async_step_reauth(
360 self, entry_data: Mapping[str, Any]
361 ) -> ConfigFlowResult:
362 """Handle configuration by re-auth."""
363 return await self.async_step_reauth_confirm()
365 async
def async_step_reauth_confirm(
366 self, user_input: dict[str, Any] |
None =
None
367 ) -> ConfigFlowResult:
368 """Dialog that informs the user that reauth is required."""
369 errors: dict[str, str] = {}
371 host = reauth_entry.data[CONF_HOST]
374 if user_input
is not None:
376 info = await self._async_get_info(host, port)
377 except (DeviceConnectionError, InvalidAuthError):
381 user_input[CONF_USERNAME] =
"admin"
384 except (DeviceConnectionError, InvalidAuthError):
386 except MacAddressMismatchError:
390 reauth_entry, data_updates=user_input
395 vol.Required(CONF_USERNAME): str,
396 vol.Required(CONF_PASSWORD): str,
399 schema = {vol.Required(CONF_PASSWORD): str}
402 step_id=
"reauth_confirm",
403 data_schema=vol.Schema(schema),
407 async
def async_step_reconfigure(
408 self, user_input: dict[str, Any] |
None =
None
409 ) -> ConfigFlowResult:
410 """Handle a reconfiguration flow initialized by the user."""
413 self.
hosthost = reconfigure_entry.data[CONF_HOST]
414 self.
portport = reconfigure_entry.data.get(CONF_PORT, DEFAULT_HTTP_PORT)
416 if user_input
is not None:
417 host = user_input[CONF_HOST]
418 port = user_input.get(CONF_PORT, DEFAULT_HTTP_PORT)
420 info = await self._async_get_info(host, port)
421 except DeviceConnectionError:
422 errors[
"base"] =
"cannot_connect"
423 except CustomPortNotSupported:
424 errors[
"base"] =
"custom_port_not_supported"
431 data_updates={CONF_HOST: host, CONF_PORT: port},
435 step_id=
"reconfigure",
436 data_schema=vol.Schema(
438 vol.Required(CONF_HOST, default=self.
hosthost): str,
439 vol.Required(CONF_PORT, default=self.
portport): vol.Coerce(int),
442 description_placeholders={
"device_name": reconfigure_entry.title},
446 async
def _async_get_info(self, host: str, port: int) -> dict[str, Any]:
447 """Get info from shelly device."""
453 """Get the options flow for this handler."""
459 """Return options flow support for this handler."""
462 and not config_entry.data.get(CONF_SLEEP_PERIOD)
463 and config_entry.data.get(
"model") != MODEL_WALL_DISPLAY
468 """Handle the option flow for shelly."""
471 self, user_input: dict[str, Any] |
None =
None
472 ) -> ConfigFlowResult:
473 """Handle options flow."""
474 if user_input
is not None:
479 data_schema=vol.Schema(
482 CONF_BLE_SCANNER_MODE,
484 CONF_BLE_SCANNER_MODE, BLEScannerMode.DISABLED
488 options=BLE_SCANNER_OPTIONS,
489 translation_key=CONF_BLE_SCANNER_MODE,
ConfigFlowResult async_step_init(self, dict[str, Any]|None user_input=None)
ConfigFlowResult async_step_user(self, dict[str, Any]|None user_input=None)
bool async_supports_options_flow(cls, ConfigEntry config_entry)
None _abort_if_unique_id_configured(self, dict[str, Any]|None updates=None, bool reload_on_update=True, *str error="already_configured")
ConfigEntry _get_reauth_entry(self)
None _set_confirm_only(self)
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_abort(self, *str reason, Mapping[str, str]|None description_placeholders=None)
ConfigFlowResult async_step_zeroconf(self, ZeroconfServiceInfo discovery_info)
ConfigEntry _get_reconfigure_entry(self)
None _abort_if_unique_id_mismatch(self, *str reason="unique_id_mismatch", 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)
OptionsFlow async_get_options_flow(ConfigEntry config_entry)
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)
_FlowResultT async_abort(self, *str reason, Mapping[str, str]|None description_placeholders=None)
dict[str, Any]|None get_info(HomeAssistant hass)
IssData update(pyiss.ISS iss)
dict[str, Any] validate_input(HomeAssistant hass, str host, int port, dict[str, Any] info, dict[str, Any] data)
None async_reconnect_soon(HomeAssistant hass, ShellyConfigEntry entry)
bool get_info_auth(dict[str, Any] info)
WsServer get_ws_context(HomeAssistant hass)
int get_info_gen(dict[str, Any] info)
str get_model_name(dict[str, Any] info)
COAP get_coap_context(HomeAssistant hass)
int get_rpc_device_wakeup_period(dict[str, Any] status)
str|None mac_address_from_name(str name)
int get_device_entry_gen(ConfigEntry entry)
int get_http_port(MappingProxyType[str, Any] data)
int get_block_device_sleep_period(dict[str, Any] settings)
tuple[dict[str, str], dict[str, str]|None] _async_get_info(HomeAssistant hass, dict[str, Any] user_input)
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)