1 """Config flow to configure Xiaomi Miio."""
3 from __future__
import annotations
5 from collections.abc
import Mapping
10 from micloud
import MiCloud
11 from micloud.micloudexception
import MiCloudAccessDenied
12 import voluptuous
as vol
28 CONF_CLOUD_SUBDEVICES,
33 DEFAULT_CLOUD_COUNTRY,
42 from .device
import ConnectXiaomiDevice
44 _LOGGER = logging.getLogger(__name__)
47 vol.Required(CONF_TOKEN): vol.All(str, vol.Length(min=32, max=32)),
49 DEVICE_CONFIG = vol.Schema({vol.Required(CONF_HOST): str}).extend(DEVICE_SETTINGS)
50 DEVICE_MODEL_CONFIG = vol.Schema({vol.Required(CONF_MODEL): vol.In(MODELS_ALL)})
51 DEVICE_CLOUD_CONFIG = vol.Schema(
53 vol.Optional(CONF_CLOUD_USERNAME): str,
54 vol.Optional(CONF_CLOUD_PASSWORD): str,
55 vol.Optional(CONF_CLOUD_COUNTRY, default=DEFAULT_CLOUD_COUNTRY): vol.In(
58 vol.Optional(CONF_MANUAL, default=
False): bool,
64 """Options for the component."""
67 self, user_input: dict[str, Any] |
None =
None
68 ) -> ConfigFlowResult:
69 """Manage the options."""
71 if user_input
is not None:
72 use_cloud = user_input.get(CONF_CLOUD_SUBDEVICES,
False)
78 not cloud_username
or not cloud_password
or not cloud_country
80 errors[
"base"] =
"cloud_credentials_incomplete"
86 settings_schema = vol.Schema(
89 CONF_CLOUD_SUBDEVICES,
96 step_id=
"init", data_schema=settings_schema, errors=errors
101 """Handle a Xiaomi Miio config flow."""
107 self.
hosthost: str |
None =
None
108 self.
macmac: str |
None =
None
115 self.
cloud_devicescloud_devices: dict[str, dict[str, Any]] = {}
120 """Get the options flow."""
124 self, entry_data: Mapping[str, Any]
125 ) -> ConfigFlowResult:
126 """Perform reauth upon an authentication error or missing cloud credentials."""
127 self.
hosthost = entry_data[CONF_HOST]
128 self.
tokentoken = entry_data[CONF_TOKEN]
129 self.
macmac = entry_data[CONF_MAC]
130 self.
modelmodel = entry_data.get(CONF_MODEL)
134 self, user_input: dict[str, Any] |
None =
None
135 ) -> ConfigFlowResult:
136 """Dialog that informs the user that reauth is required."""
137 if user_input
is not None:
142 self, user_input: dict[str, Any] |
None =
None
143 ) -> ConfigFlowResult:
144 """Handle a flow initialized by the user."""
148 self, discovery_info: zeroconf.ZeroconfServiceInfo
149 ) -> ConfigFlowResult:
150 """Handle zeroconf discovery."""
151 name = discovery_info.name
152 self.
hosthost = discovery_info.host
153 self.
macmac = discovery_info.properties.get(
"mac")
154 if self.
macmac
is None:
155 poch = discovery_info.properties.get(
"poch",
"")
156 if (result := search(
r"mac=\w+", poch))
is not None:
157 self.
macmac = result.group(0).split(
"=")[1]
159 if not name
or not self.
hosthost
or not self.
macmac:
165 for gateway_model
in MODELS_GATEWAY:
166 if name.startswith(gateway_model.replace(
".",
"-")):
167 unique_id = self.
macmac
172 {
"title_placeholders": {
"name": f
"Gateway {self.host}"}}
177 for device_model
in MODELS_ALL_DEVICES:
178 if name.startswith(device_model.replace(
".",
"-")):
179 unique_id = self.
macmac
184 {
"title_placeholders": {
"name": f
"{device_model} {self.host}"}}
191 "Not yet supported Xiaomi Miio device '%s' discovered with host %s",
198 """Extract the cloud info."""
199 if self.
hosthost
is None:
200 self.
hosthost = cloud_device_info[
"localip"]
201 if self.
macmac
is None:
203 if self.
modelmodel
is None:
204 self.
modelmodel = cloud_device_info[
"model"]
205 if self.
namename
is None:
206 self.
namename = cloud_device_info[
"name"]
207 self.
tokentoken = cloud_device_info[
"token"]
210 self, user_input: dict[str, Any] |
None =
None
211 ) -> ConfigFlowResult:
212 """Configure a xiaomi miio device through the Miio Cloud."""
214 if user_input
is not None:
215 if user_input[CONF_MANUAL]:
218 cloud_username = user_input.get(CONF_CLOUD_USERNAME)
219 cloud_password = user_input.get(CONF_CLOUD_PASSWORD)
220 cloud_country = user_input.get(CONF_CLOUD_COUNTRY)
222 if not cloud_username
or not cloud_password
or not cloud_country:
223 errors[
"base"] =
"cloud_credentials_incomplete"
225 step_id=
"cloud", data_schema=DEVICE_CLOUD_CONFIG, errors=errors
228 miio_cloud = await self.hass.async_add_executor_job(
229 MiCloud, cloud_username, cloud_password
232 if not await self.hass.async_add_executor_job(miio_cloud.login):
233 errors[
"base"] =
"cloud_login_error"
234 except MiCloudAccessDenied:
235 errors[
"base"] =
"cloud_login_error"
237 _LOGGER.exception(
"Unexpected exception in Miio cloud login")
242 step_id=
"cloud", data_schema=DEVICE_CLOUD_CONFIG, errors=errors
246 devices_raw = await self.hass.async_add_executor_job(
247 miio_cloud.get_devices, cloud_country
250 _LOGGER.exception(
"Unexpected exception in Miio cloud get devices")
254 errors[
"base"] =
"cloud_no_devices"
256 step_id=
"cloud", data_schema=DEVICE_CLOUD_CONFIG, errors=errors
260 for device
in devices_raw:
261 if not device.get(
"parent_id"):
262 name = device[
"name"]
263 model = device[
"model"]
264 list_name = f
"{name} - {model}"
271 if self.
hosthost
is not None:
273 cloud_host = device.get(
"localip")
274 if cloud_host == self.
hosthost:
285 step_id=
"cloud", data_schema=DEVICE_CLOUD_CONFIG, errors=errors
289 self, user_input: dict[str, Any] |
None =
None
290 ) -> ConfigFlowResult:
291 """Handle multiple cloud devices found."""
292 errors: dict[str, str] = {}
293 if user_input
is not None:
294 cloud_device = self.
cloud_devicescloud_devices[user_input[
"select_device"]]
298 select_schema = vol.Schema(
303 step_id=
"select", data_schema=select_schema, errors=errors
307 self, user_input: dict[str, Any] |
None =
None
308 ) -> ConfigFlowResult:
309 """Configure a xiaomi miio device Manually."""
310 errors: dict[str, str] = {}
311 if user_input
is not None:
312 self.
tokentoken = user_input[CONF_TOKEN]
313 if user_input.get(CONF_HOST):
314 self.
hosthost = user_input[CONF_HOST]
319 schema = vol.Schema(DEVICE_SETTINGS)
321 schema = DEVICE_CONFIG
326 self, user_input: dict[str, Any] |
None =
None
327 ) -> ConfigFlowResult:
328 """Connect to a xiaomi miio device."""
329 errors: dict[str, str] = {}
330 if self.
hosthost
is None or self.
tokentoken
is None:
333 if user_input
is not None:
334 self.
modelmodel = user_input[CONF_MODEL]
339 await connect_device_class.async_connect_device(self.
hosthost, self.
tokentoken)
340 except AuthException:
341 if self.
modelmodel
is None:
342 errors[
"base"] =
"wrong_token"
343 except SetupException:
344 if self.
modelmodel
is None:
345 errors[
"base"] =
"cannot_connect"
347 _LOGGER.exception(
"Unexpected exception in connect Xiaomi device")
350 device_info = connect_device_class.device_info
352 if self.
modelmodel
is None and device_info
is not None:
353 self.
modelmodel = device_info.model
355 if self.
modelmodel
is None and not errors:
356 errors[
"base"] =
"cannot_connect"
360 step_id=
"connect", data_schema=DEVICE_MODEL_CONFIG, errors=errors
363 if self.
macmac
is None and device_info
is not None:
366 unique_id = self.
macmac
368 unique_id, raise_on_progress=
False
371 data = existing_entry.data.copy()
372 data[CONF_HOST] = self.
hosthost
373 data[CONF_TOKEN] = self.
tokentoken
384 if self.
namename
is None:
388 for gateway_model
in MODELS_GATEWAY:
389 if self.
modelmodel.startswith(gateway_model):
390 flow_type = CONF_GATEWAY
392 if flow_type
is None:
393 for device_model
in MODELS_ALL_DEVICES:
394 if self.
modelmodel.startswith(device_model):
395 flow_type = CONF_DEVICE
397 if flow_type
is not None:
401 CONF_FLOW_TYPE: flow_type,
402 CONF_HOST: self.
hosthost,
403 CONF_TOKEN: self.
tokentoken,
404 CONF_MODEL: self.
modelmodel,
405 CONF_MAC: self.
macmac,
412 errors[
"base"] =
"unknown_device"
414 step_id=
"connect", data_schema=DEVICE_MODEL_CONFIG, errors=errors
ConfigFlowResult async_step_init(self, dict[str, Any]|None user_input=None)
ConfigFlowResult async_step_reauth(self, Mapping[str, Any] entry_data)
ConfigFlowResult async_step_cloud(self, dict[str, Any]|None user_input=None)
None extract_cloud_info(self, dict[str, Any] cloud_device_info)
ConfigFlowResult async_step_user(self, dict[str, Any]|None user_input=None)
ConfigFlowResult async_step_zeroconf(self, zeroconf.ZeroconfServiceInfo discovery_info)
ConfigFlowResult async_step_reauth_confirm(self, dict[str, Any]|None user_input=None)
ConfigFlowResult async_step_manual(self, dict[str, Any]|None user_input=None)
OptionsFlowHandler async_get_options_flow(ConfigEntry config_entry)
ConfigFlowResult async_step_select(self, dict[str, Any]|None user_input=None)
ConfigFlowResult async_step_connect(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_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_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)
IssData update(pyiss.ISS iss)