Home Assistant Unofficial Reference 2024.12.1
config_flow.py
Go to the documentation of this file.
1 """Config flow for Aurora ABB PowerOne integration."""
2 
3 from __future__ import annotations
4 
5 from collections.abc import Mapping
6 import logging
7 from typing import TYPE_CHECKING, Any
8 
9 from aurorapy.client import AuroraError, AuroraSerialClient
10 import serial.tools.list_ports
11 import voluptuous as vol
12 
13 from homeassistant.config_entries import ConfigFlow, ConfigFlowResult
14 from homeassistant.const import ATTR_SERIAL_NUMBER, CONF_ADDRESS, CONF_PORT
15 from homeassistant.core import HomeAssistant
16 
17 from .const import (
18  ATTR_FIRMWARE,
19  ATTR_MODEL,
20  DEFAULT_ADDRESS,
21  DEFAULT_INTEGRATION_TITLE,
22  DOMAIN,
23  MAX_ADDRESS,
24  MIN_ADDRESS,
25 )
26 
27 _LOGGER = logging.getLogger(__name__)
28 
29 
31  hass: HomeAssistant, data: Mapping[str, Any]
32 ) -> dict[str, str]:
33  """Validate the user input allows us to connect.
34 
35  Data has the keys from DATA_SCHEMA with values provided by the user.
36  """
37  comport = data[CONF_PORT]
38  address = data[CONF_ADDRESS]
39  _LOGGER.debug("Initialising com port=%s", comport)
40  ret = {}
41  ret["title"] = DEFAULT_INTEGRATION_TITLE
42  try:
43  client = AuroraSerialClient(address, comport, parity="N", timeout=1)
44  client.connect()
45  ret[ATTR_SERIAL_NUMBER] = client.serial_number()
46  ret[ATTR_MODEL] = f"{client.version()} ({client.pn()})"
47  ret[ATTR_FIRMWARE] = client.firmware(1)
48  _LOGGER.debug("Returning device info=%s", ret)
49  except AuroraError:
50  _LOGGER.warning("Could not connect to device=%s", comport)
51  raise
52  finally:
53  if client.serline.isOpen():
54  client.close()
55 
56  # Return info we want to store in the config entry.
57  return ret
58 
59 
60 def scan_comports() -> tuple[list[str] | None, str | None]:
61  """Find and store available com ports for the GUI dropdown."""
62  com_ports = serial.tools.list_ports.comports(include_links=True)
63  com_ports_list = []
64  for port in com_ports:
65  com_ports_list.append(port.device)
66  _LOGGER.debug("COM port option: %s", port.device)
67  if len(com_ports_list) > 0:
68  return com_ports_list, com_ports_list[0]
69  _LOGGER.warning("No com ports found. Need a valid RS485 device to communicate")
70  return None, None
71 
72 
73 class AuroraABBConfigFlow(ConfigFlow, domain=DOMAIN):
74  """Handle a config flow for Aurora ABB PowerOne."""
75 
76  VERSION = 1
77 
78  def __init__(self) -> None:
79  """Initialise the config flow."""
80  self._com_ports_list: list[str] | None = None
81  self._default_com_port_default_com_port: str | None = None
82 
83  async def async_step_user(
84  self, user_input: dict[str, Any] | None = None
85  ) -> ConfigFlowResult:
86  """Handle a flow initialised by the user."""
87 
88  errors = {}
89  if self._com_ports_list is None:
90  result = await self.hass.async_add_executor_job(scan_comports)
91  self._com_ports_list, self._default_com_port_default_com_port = result
92  if self._default_com_port_default_com_port is None:
93  return self.async_abortasync_abortasync_abort(reason="no_serial_ports")
94  if TYPE_CHECKING:
95  assert isinstance(self._com_ports_list, list)
96 
97  # Handle the initial step.
98  if user_input is not None:
99  try:
100  info = await self.hass.async_add_executor_job(
101  validate_and_connect, self.hass, user_input
102  )
103  except OSError as error:
104  if error.errno == 19: # No such device.
105  errors["base"] = "invalid_serial_port"
106  except AuroraError as error:
107  if "could not open port" in str(error):
108  errors["base"] = "cannot_open_serial_port"
109  elif "No response after" in str(error):
110  errors["base"] = "cannot_connect" # could be dark
111  else:
112  _LOGGER.error(
113  "Unable to communicate with Aurora ABB Inverter at %s: %s %s",
114  user_input[CONF_PORT],
115  type(error),
116  error,
117  )
118  errors["base"] = "cannot_connect"
119  else:
120  info.update(user_input)
121  # Bomb out early if someone has already set up this device.
122  device_unique_id = info["serial_number"]
123  await self.async_set_unique_idasync_set_unique_id(device_unique_id)
124  self._abort_if_unique_id_configured_abort_if_unique_id_configured()
125  return self.async_create_entryasync_create_entryasync_create_entry(title=info["title"], data=info)
126 
127  # If no user input, must be first pass through the config. Show initial form.
128  config_options = {
129  vol.Required(CONF_PORT, default=self._default_com_port_default_com_port): vol.In(
130  self._com_ports_list
131  ),
132  vol.Required(CONF_ADDRESS, default=DEFAULT_ADDRESS): vol.In(
133  range(MIN_ADDRESS, MAX_ADDRESS + 1)
134  ),
135  }
136  schema = vol.Schema(config_options)
137 
138  return self.async_show_formasync_show_formasync_show_form(step_id="user", data_schema=schema, errors=errors)
ConfigFlowResult async_step_user(self, dict[str, Any]|None user_input=None)
Definition: config_flow.py:85
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_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)
dict[str, str] validate_and_connect(HomeAssistant hass, Mapping[str, Any] data)
Definition: config_flow.py:32
tuple[list[str]|None, str|None] scan_comports()
Definition: config_flow.py:60