Home Assistant Unofficial Reference 2024.12.1
config_flow.py
Go to the documentation of this file.
1 """Config flow to configure Xiaomi Aqara."""
2 
3 import logging
4 from socket import gaierror
5 from typing import Any
6 
7 import voluptuous as vol
8 from xiaomi_gateway import MULTICAST_PORT, XiaomiGateway, XiaomiGatewayDiscovery
9 
10 from homeassistant.components import zeroconf
11 from homeassistant.config_entries import ConfigFlow, ConfigFlowResult
12 from homeassistant.const import CONF_HOST, CONF_MAC, CONF_NAME, CONF_PORT, CONF_PROTOCOL
13 from homeassistant.core import callback
14 from homeassistant.helpers.device_registry import format_mac
15 
16 from .const import (
17  CONF_INTERFACE,
18  CONF_KEY,
19  CONF_SID,
20  DEFAULT_DISCOVERY_RETRY,
21  DOMAIN,
22  ZEROCONF_ACPARTNER,
23  ZEROCONF_GATEWAY,
24 )
25 
26 _LOGGER = logging.getLogger(__name__)
27 
28 DEFAULT_GATEWAY_NAME = "Xiaomi Aqara Gateway"
29 DEFAULT_INTERFACE = "any"
30 
31 
32 GATEWAY_CONFIG = vol.Schema(
33  {vol.Optional(CONF_INTERFACE, default=DEFAULT_INTERFACE): str}
34 )
35 CONFIG_HOST = {
36  vol.Optional(CONF_HOST): str,
37  vol.Optional(CONF_MAC): str,
38 }
39 GATEWAY_CONFIG_HOST = GATEWAY_CONFIG.extend(CONFIG_HOST)
40 GATEWAY_SETTINGS = vol.Schema(
41  {
42  vol.Optional(CONF_KEY): vol.All(str, vol.Length(min=16, max=16)),
43  vol.Optional(CONF_NAME, default=DEFAULT_GATEWAY_NAME): str,
44  }
45 )
46 
47 
48 class XiaomiAqaraFlowHandler(ConfigFlow, domain=DOMAIN):
49  """Handle a Xiaomi Aqara config flow."""
50 
51  VERSION = 1
52 
53  selected_gateway: XiaomiGateway
54  gateways: dict[str, XiaomiGateway]
55 
56  def __init__(self) -> None:
57  """Initialize."""
58  self.hosthost: str | None = None
59  self.interfaceinterface = DEFAULT_INTERFACE
60  self.sidsid: str | None = None
61 
62  @callback
63  def async_show_form_step_user(self, errors):
64  """Show the form belonging to the user step."""
65  schema = GATEWAY_CONFIG
66  if (self.hosthost is None and self.sidsid is None) or errors:
67  schema = GATEWAY_CONFIG_HOST
68 
69  return self.async_show_formasync_show_formasync_show_form(step_id="user", data_schema=schema, errors=errors)
70 
71  async def async_step_user(
72  self, user_input: dict[str, Any] | None = None
73  ) -> ConfigFlowResult:
74  """Handle a flow initialized by the user."""
75  errors: dict[str, str] = {}
76  if user_input is None:
77  return self.async_show_form_step_userasync_show_form_step_user(errors)
78 
79  self.interfaceinterface = user_input[CONF_INTERFACE]
80 
81  # allow optional manual setting of host and mac
82  if self.hosthost is None:
83  self.hosthost = user_input.get(CONF_HOST)
84  if self.sidsid is None:
85  # format sid from mac_address
86  if (mac_address := user_input.get(CONF_MAC)) is not None:
87  self.sidsid = format_mac(mac_address).replace(":", "")
88 
89  # if host is already known by zeroconf discovery or manual optional settings
90  if self.hosthost is not None and self.sidsid is not None:
91  # Connect to Xiaomi Aqara Gateway
92  self.selected_gatewayselected_gateway = await self.hass.async_add_executor_job(
93  XiaomiGateway,
94  self.hosthost,
95  self.sidsid,
96  None,
97  DEFAULT_DISCOVERY_RETRY,
98  self.interfaceinterface,
99  MULTICAST_PORT,
100  None,
101  )
102 
103  if self.selected_gatewayselected_gateway.connection_error:
104  errors[CONF_HOST] = "invalid_host"
105  if self.selected_gatewayselected_gateway.mac_error:
106  errors[CONF_MAC] = "invalid_mac"
107  if errors:
108  return self.async_show_form_step_userasync_show_form_step_user(errors)
109 
110  return await self.async_step_settingsasync_step_settings()
111 
112  # Discover Xiaomi Aqara Gateways in the network to get required SIDs.
113  xiaomi = XiaomiGatewayDiscovery(self.interfaceinterface)
114  try:
115  await self.hass.async_add_executor_job(xiaomi.discover_gateways)
116  except gaierror:
117  errors[CONF_INTERFACE] = "invalid_interface"
118  return self.async_show_form_step_userasync_show_form_step_user(errors)
119 
120  self.gatewaysgateways = xiaomi.gateways
121 
122  if len(self.gatewaysgateways) == 1:
123  self.selected_gatewayselected_gateway = list(self.gatewaysgateways.values())[0]
124  self.sidsid = self.selected_gatewayselected_gateway.sid
125  return await self.async_step_settingsasync_step_settings()
126  if len(self.gatewaysgateways) > 1:
127  return await self.async_step_selectasync_step_select()
128 
129  errors["base"] = "discovery_error"
130  return self.async_show_form_step_userasync_show_form_step_user(errors)
131 
132  async def async_step_select(
133  self, user_input: dict[str, str] | None = None
134  ) -> ConfigFlowResult:
135  """Handle multiple aqara gateways found."""
136  errors: dict[str, str] = {}
137  if user_input is not None:
138  ip_adress = user_input["select_ip"]
139  self.selected_gatewayselected_gateway = self.gatewaysgateways[ip_adress]
140  self.sidsid = self.selected_gatewayselected_gateway.sid
141  return await self.async_step_settingsasync_step_settings()
142 
143  select_schema = vol.Schema(
144  {
145  vol.Required("select_ip"): vol.In(
146  [gateway.ip_adress for gateway in self.gatewaysgateways.values()]
147  )
148  }
149  )
150 
151  return self.async_show_formasync_show_formasync_show_form(
152  step_id="select", data_schema=select_schema, errors=errors
153  )
154 
156  self, discovery_info: zeroconf.ZeroconfServiceInfo
157  ) -> ConfigFlowResult:
158  """Handle zeroconf discovery."""
159  name = discovery_info.name
160  self.hosthost = discovery_info.host
161  mac_address = discovery_info.properties.get("mac")
162 
163  if not name or not self.hosthost or not mac_address:
164  return self.async_abortasync_abortasync_abort(reason="not_xiaomi_aqara")
165 
166  # Check if the discovered device is an xiaomi aqara gateway.
167  if not (name.startswith((ZEROCONF_GATEWAY, ZEROCONF_ACPARTNER))):
168  _LOGGER.debug(
169  (
170  "Xiaomi device '%s' discovered with host %s, not identified as"
171  " xiaomi aqara gateway"
172  ),
173  name,
174  self.hosthost,
175  )
176  return self.async_abortasync_abortasync_abort(reason="not_xiaomi_aqara")
177 
178  # format mac (include semicolns and make lowercase)
179  mac_address = format_mac(mac_address)
180 
181  # format sid from mac_address
182  self.sidsid = mac_address.replace(":", "")
183 
184  unique_id = mac_address
185  await self.async_set_unique_idasync_set_unique_id(unique_id)
186  self._abort_if_unique_id_configured_abort_if_unique_id_configured(
187  {CONF_HOST: self.hosthost, CONF_MAC: mac_address}
188  )
189 
190  self.context.update({"title_placeholders": {"name": self.hosthost}})
191 
192  return await self.async_step_userasync_step_userasync_step_user()
193 
195  self, user_input: dict[str, str] | None = None
196  ) -> ConfigFlowResult:
197  """Specify settings and connect aqara gateway."""
198  errors = {}
199  if user_input is not None:
200  # get all required data
201  name = user_input[CONF_NAME]
202  key = user_input.get(CONF_KEY)
203  ip_adress = self.selected_gatewayselected_gateway.ip_adress
204  port = self.selected_gatewayselected_gateway.port
205  protocol = self.selected_gatewayselected_gateway.proto
206 
207  if key is not None:
208  # validate key by issuing stop ringtone playback command.
209  self.selected_gatewayselected_gateway.key = key
210  valid_key = self.selected_gatewayselected_gateway.write_to_hub(self.sidsid, mid=10000)
211  else:
212  valid_key = True
213 
214  if valid_key:
215  # format_mac, for a gateway the sid equals the mac address
216  mac_address = format_mac(self.sidsid)
217 
218  # set unique_id
219  unique_id = mac_address
220  await self.async_set_unique_idasync_set_unique_id(unique_id)
221  self._abort_if_unique_id_configured_abort_if_unique_id_configured()
222 
223  return self.async_create_entryasync_create_entryasync_create_entry(
224  title=name,
225  data={
226  CONF_HOST: ip_adress,
227  CONF_PORT: port,
228  CONF_MAC: mac_address,
229  CONF_INTERFACE: self.interfaceinterface,
230  CONF_PROTOCOL: protocol,
231  CONF_KEY: key,
232  CONF_SID: self.sidsid,
233  },
234  )
235 
236  errors[CONF_KEY] = "invalid_key"
237 
238  return self.async_show_formasync_show_formasync_show_form(
239  step_id="settings", data_schema=GATEWAY_SETTINGS, errors=errors
240  )
ConfigFlowResult async_step_select(self, dict[str, str]|None user_input=None)
Definition: config_flow.py:134
ConfigFlowResult async_step_settings(self, dict[str, str]|None user_input=None)
Definition: config_flow.py:196
ConfigFlowResult async_step_zeroconf(self, zeroconf.ZeroconfServiceInfo discovery_info)
Definition: config_flow.py:157
ConfigFlowResult async_step_user(self, dict[str, Any]|None user_input=None)
Definition: config_flow.py:73
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)
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)
IssData update(pyiss.ISS iss)
Definition: __init__.py:33