Home Assistant Unofficial Reference 2024.12.1
config_flow.py
Go to the documentation of this file.
1 """Config flow to configure the RainMachine component."""
2 
3 from __future__ import annotations
4 
5 from typing import Any
6 
7 from regenmaschine import Client
8 from regenmaschine.controller import Controller
9 from regenmaschine.errors import RainMachineError
10 import voluptuous as vol
11 
12 from homeassistant.components import zeroconf
13 from homeassistant.config_entries import (
14  ConfigEntry,
15  ConfigFlow,
16  ConfigFlowResult,
17  OptionsFlow,
18 )
19 from homeassistant.const import CONF_IP_ADDRESS, CONF_PASSWORD, CONF_PORT, CONF_SSL
20 from homeassistant.core import HomeAssistant, callback
21 from homeassistant.helpers import aiohttp_client, config_validation as cv
22 
23 from .const import (
24  CONF_ALLOW_INACTIVE_ZONES_TO_RUN,
25  CONF_DEFAULT_ZONE_RUN_TIME,
26  CONF_USE_APP_RUN_TIMES,
27  DEFAULT_PORT,
28  DEFAULT_ZONE_RUN,
29  DOMAIN,
30 )
31 
32 
33 @callback
34 def get_client_controller(client: Client) -> Controller:
35  """Return the first local controller."""
36  return next(iter(client.controllers.values()))
37 
38 
40  hass: HomeAssistant, ip_address: str, password: str, port: int, ssl: bool
41 ) -> Controller | None:
42  """Auth and fetch the mac address from the controller."""
43  websession = aiohttp_client.async_get_clientsession(hass)
44  client = Client(session=websession)
45  try:
46  await client.load_local(ip_address, password, port=port, use_ssl=ssl)
47  except RainMachineError:
48  return None
49 
50  return get_client_controller(client)
51 
52 
53 class RainMachineFlowHandler(ConfigFlow, domain=DOMAIN):
54  """Handle a RainMachine config flow."""
55 
56  VERSION = 2
57 
58  discovered_ip_address: str | None = None
59 
60  @staticmethod
61  @callback
63  config_entry: ConfigEntry,
64  ) -> RainMachineOptionsFlowHandler:
65  """Define the config flow to handle options."""
67 
68  async def async_step_homekit(
69  self, discovery_info: zeroconf.ZeroconfServiceInfo
70  ) -> ConfigFlowResult:
71  """Handle a flow initialized by homekit discovery."""
72  return await self.async_step_homekit_zeroconfasync_step_homekit_zeroconf(discovery_info)
73 
75  self, discovery_info: zeroconf.ZeroconfServiceInfo
76  ) -> ConfigFlowResult:
77  """Handle discovery via zeroconf."""
78  return await self.async_step_homekit_zeroconfasync_step_homekit_zeroconf(discovery_info)
79 
81  self, discovery_info: zeroconf.ZeroconfServiceInfo
82  ) -> ConfigFlowResult:
83  """Handle discovery via zeroconf."""
84  ip_address = discovery_info.host
85 
86  self._async_abort_entries_match_async_abort_entries_match({CONF_IP_ADDRESS: ip_address})
87  # Handle IP change
88  for entry in self._async_current_entries_async_current_entries(include_ignore=False):
89  # Try our existing credentials to check for ip change
90  if controller := await async_get_controller(
91  self.hass,
92  ip_address,
93  entry.data[CONF_PASSWORD],
94  entry.data[CONF_PORT],
95  entry.data.get(CONF_SSL, True),
96  ):
97  await self.async_set_unique_idasync_set_unique_id(controller.mac)
98  self._abort_if_unique_id_configured_abort_if_unique_id_configured(
99  updates={CONF_IP_ADDRESS: ip_address}, reload_on_update=False
100  )
101 
102  # A new rain machine: We will change out the unique id
103  # for the mac address once we authenticate, however we want to
104  # prevent multiple different rain machines on the same network
105  # from being shown in discovery
106  await self.async_set_unique_idasync_set_unique_id(ip_address)
107  self._abort_if_unique_id_configured_abort_if_unique_id_configured()
108  self.discovered_ip_addressdiscovered_ip_address = ip_address
109  return await self.async_step_userasync_step_userasync_step_user()
110 
111  @callback
112  def _async_generate_schema(self) -> vol.Schema:
113  """Generate schema."""
114  return vol.Schema(
115  {
116  vol.Required(CONF_IP_ADDRESS, default=self.discovered_ip_addressdiscovered_ip_address): str,
117  vol.Required(CONF_PASSWORD): str,
118  vol.Optional(CONF_PORT, default=DEFAULT_PORT): int,
119  }
120  )
121 
122  async def async_step_user(
123  self, user_input: dict[str, Any] | None = None
124  ) -> ConfigFlowResult:
125  """Handle the start of the config flow."""
126  errors = {}
127  if user_input:
128  self._async_abort_entries_match_async_abort_entries_match(
129  {CONF_IP_ADDRESS: user_input[CONF_IP_ADDRESS]}
130  )
131  controller = await async_get_controller(
132  self.hass,
133  user_input[CONF_IP_ADDRESS],
134  user_input[CONF_PASSWORD],
135  user_input[CONF_PORT],
136  user_input.get(CONF_SSL, True),
137  )
138  if controller:
139  await self.async_set_unique_idasync_set_unique_id(controller.mac)
140  self._abort_if_unique_id_configured_abort_if_unique_id_configured()
141 
142  # Unfortunately, RainMachine doesn't provide a way to refresh the
143  # access token without using the IP address and password, so we have to
144  # store it:
145  return self.async_create_entryasync_create_entryasync_create_entry(
146  title=controller.name.capitalize(),
147  data={
148  CONF_IP_ADDRESS: user_input[CONF_IP_ADDRESS],
149  CONF_PASSWORD: user_input[CONF_PASSWORD],
150  CONF_PORT: user_input[CONF_PORT],
151  CONF_SSL: user_input.get(CONF_SSL, True),
152  CONF_DEFAULT_ZONE_RUN_TIME: user_input.get(
153  CONF_DEFAULT_ZONE_RUN_TIME, DEFAULT_ZONE_RUN
154  ),
155  },
156  )
157 
158  errors = {CONF_PASSWORD: "invalid_auth"}
159 
160  if self.discovered_ip_addressdiscovered_ip_address:
161  self.context["title_placeholders"] = {"ip": self.discovered_ip_addressdiscovered_ip_address}
162 
163  return self.async_show_formasync_show_formasync_show_form(
164  step_id="user", data_schema=self._async_generate_schema_async_generate_schema(), errors=errors
165  )
166 
167 
169  """Handle a RainMachine options flow."""
170 
171  async def async_step_init(
172  self, user_input: dict[str, Any] | None = None
173  ) -> ConfigFlowResult:
174  """Manage the options."""
175  if user_input is not None:
176  return self.async_create_entryasync_create_entry(data=user_input)
177 
178  return self.async_show_formasync_show_form(
179  step_id="init",
180  data_schema=vol.Schema(
181  {
182  vol.Optional(
183  CONF_DEFAULT_ZONE_RUN_TIME,
184  default=self.config_entryconfig_entryconfig_entry.options.get(
185  CONF_DEFAULT_ZONE_RUN_TIME
186  ),
187  ): cv.positive_int,
188  vol.Optional(
189  CONF_USE_APP_RUN_TIMES,
190  default=self.config_entryconfig_entryconfig_entry.options.get(CONF_USE_APP_RUN_TIMES),
191  ): bool,
192  vol.Optional(
193  CONF_ALLOW_INACTIVE_ZONES_TO_RUN,
194  default=self.config_entryconfig_entryconfig_entry.options.get(
195  CONF_ALLOW_INACTIVE_ZONES_TO_RUN
196  ),
197  ): bool,
198  }
199  ),
200  )
ConfigFlowResult async_step_user(self, dict[str, Any]|None user_input=None)
Definition: config_flow.py:124
ConfigFlowResult async_step_zeroconf(self, zeroconf.ZeroconfServiceInfo discovery_info)
Definition: config_flow.py:76
RainMachineOptionsFlowHandler async_get_options_flow(ConfigEntry config_entry)
Definition: config_flow.py:64
ConfigFlowResult async_step_homekit(self, zeroconf.ZeroconfServiceInfo discovery_info)
Definition: config_flow.py:70
ConfigFlowResult async_step_homekit_zeroconf(self, zeroconf.ZeroconfServiceInfo discovery_info)
Definition: config_flow.py:82
ConfigFlowResult async_step_init(self, dict[str, Any]|None user_input=None)
Definition: config_flow.py:173
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)
list[ConfigEntry] _async_current_entries(self, bool|None include_ignore=None)
ConfigFlowResult async_step_user(self, dict[str, Any]|None user_input=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)
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)
Controller get_client_controller(Client client)
Definition: config_flow.py:34
Controller|None async_get_controller(HomeAssistant hass, str ip_address, str password, int port, bool ssl)
Definition: config_flow.py:41