Home Assistant Unofficial Reference 2024.12.1
config_flow.py
Go to the documentation of this file.
1 """Config flow for Gardena Bluetooth integration."""
2 
3 from __future__ import annotations
4 
5 import logging
6 from typing import Any
7 
8 from gardena_bluetooth.client import Client
9 from gardena_bluetooth.const import PRODUCT_NAMES, DeviceInformation, ScanService
10 from gardena_bluetooth.exceptions import CharacteristicNotFound, CommunicationFailure
11 from gardena_bluetooth.parse import ManufacturerData, ProductType
12 import voluptuous as vol
13 
15  BluetoothServiceInfo,
16  async_discovered_service_info,
17 )
18 from homeassistant.config_entries import ConfigFlow, ConfigFlowResult
19 from homeassistant.const import CONF_ADDRESS
20 from homeassistant.data_entry_flow import AbortFlow
21 
22 from . import get_connection
23 from .const import DOMAIN
24 
25 _LOGGER = logging.getLogger(__name__)
26 
27 
28 def _is_supported(discovery_info: BluetoothServiceInfo):
29  """Check if device is supported."""
30  if ScanService not in discovery_info.service_uuids:
31  return False
32 
33  if not (data := discovery_info.manufacturer_data.get(ManufacturerData.company)):
34  _LOGGER.debug("Missing manufacturer data: %s", discovery_info)
35  return False
36 
37  manufacturer_data = ManufacturerData.decode(data)
38  product_type = ProductType.from_manufacturer_data(manufacturer_data)
39 
40  if product_type not in (
41  ProductType.PUMP,
42  ProductType.VALVE,
43  ProductType.WATER_COMPUTER,
44  ):
45  _LOGGER.debug("Unsupported device: %s", manufacturer_data)
46  return False
47 
48  return True
49 
50 
51 def _get_name(discovery_info: BluetoothServiceInfo):
52  data = discovery_info.manufacturer_data[ManufacturerData.company]
53  manufacturer_data = ManufacturerData.decode(data)
54  product_type = ProductType.from_manufacturer_data(manufacturer_data)
55 
56  return PRODUCT_NAMES.get(product_type, "Gardena Device")
57 
58 
59 class GardenaBluetoothConfigFlow(ConfigFlow, domain=DOMAIN):
60  """Handle a config flow for Gardena Bluetooth."""
61 
62  VERSION = 1
63 
64  def __init__(self) -> None:
65  """Initialize the config flow."""
66  self.devicesdevices: dict[str, str] = {}
67  self.addressaddress: str | None
68 
69  async def async_read_data(self):
70  """Try to connect to device and extract information."""
71  client = Client(get_connection(self.hass, self.addressaddress))
72  try:
73  model = await client.read_char(DeviceInformation.model_number)
74  _LOGGER.debug("Found device with model: %s", model)
75  except (CharacteristicNotFound, CommunicationFailure) as exception:
76  raise AbortFlow(
77  "cannot_connect", description_placeholders={"error": str(exception)}
78  ) from exception
79  finally:
80  await client.disconnect()
81 
82  return {CONF_ADDRESS: self.addressaddress}
83 
85  self, discovery_info: BluetoothServiceInfo
86  ) -> ConfigFlowResult:
87  """Handle the bluetooth discovery step."""
88  _LOGGER.debug("Discovered device: %s", discovery_info)
89  if not _is_supported(discovery_info):
90  return self.async_abortasync_abortasync_abort(reason="no_devices_found")
91 
92  self.addressaddress = discovery_info.address
93  self.devicesdevices = {discovery_info.address: _get_name(discovery_info)}
94  await self.async_set_unique_idasync_set_unique_id(self.addressaddress)
95  self._abort_if_unique_id_configured_abort_if_unique_id_configured()
96  return await self.async_step_confirmasync_step_confirm()
97 
98  async def async_step_confirm(
99  self, user_input: dict[str, Any] | None = None
100  ) -> ConfigFlowResult:
101  """Confirm discovery."""
102  assert self.addressaddress
103  title = self.devicesdevices[self.addressaddress]
104 
105  if user_input is not None:
106  data = await self.async_read_dataasync_read_data()
107  return self.async_create_entryasync_create_entryasync_create_entry(title=title, data=data)
108 
109  self.context["title_placeholders"] = {
110  "name": title,
111  }
112 
113  self._set_confirm_only_set_confirm_only()
114  return self.async_show_formasync_show_formasync_show_form(
115  step_id="confirm",
116  description_placeholders=self.context["title_placeholders"],
117  )
118 
119  async def async_step_user(
120  self, user_input: dict[str, Any] | None = None
121  ) -> ConfigFlowResult:
122  """Handle the initial step."""
123  if user_input is not None:
124  self.addressaddress = user_input[CONF_ADDRESS]
125  await self.async_set_unique_idasync_set_unique_id(self.addressaddress, raise_on_progress=False)
126  self._abort_if_unique_id_configured_abort_if_unique_id_configured()
127  return await self.async_step_confirmasync_step_confirm()
128 
129  current_addresses = self._async_current_ids_async_current_ids()
130  for discovery_info in async_discovered_service_info(self.hass):
131  address = discovery_info.address
132  if address in current_addresses or not _is_supported(discovery_info):
133  continue
134 
135  self.devicesdevices[address] = _get_name(discovery_info)
136 
137  if not self.devicesdevices:
138  return self.async_abortasync_abortasync_abort(reason="no_devices_found")
139 
140  return self.async_show_formasync_show_formasync_show_form(
141  step_id="user",
142  data_schema=vol.Schema(
143  {
144  vol.Required(CONF_ADDRESS): vol.In(self.devicesdevices),
145  },
146  ),
147  )
ConfigFlowResult async_step_confirm(self, dict[str, Any]|None user_input=None)
Definition: config_flow.py:100
ConfigFlowResult async_step_user(self, dict[str, Any]|None user_input=None)
Definition: config_flow.py:121
ConfigFlowResult async_step_bluetooth(self, BluetoothServiceInfo discovery_info)
Definition: config_flow.py:86
None _abort_if_unique_id_configured(self, dict[str, Any]|None updates=None, bool reload_on_update=True, *str error="already_configured")
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)
str
_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)
Iterable[BluetoothServiceInfoBleak] async_discovered_service_info(HomeAssistant hass, bool connectable=True)
Definition: api.py:72
def _is_supported(BluetoothServiceInfo discovery_info)
Definition: config_flow.py:28
def _get_name(BluetoothServiceInfo discovery_info)
Definition: config_flow.py:51
CachedConnection get_connection(HomeAssistant hass, str address)
Definition: __init__.py:38