Home Assistant Unofficial Reference 2024.12.1
config_flow.py
Go to the documentation of this file.
1 """Config flow for Nibe Heat Pump integration."""
2 
3 from __future__ import annotations
4 
5 from typing import Any
6 
7 from nibe.connection.modbus import Modbus
8 from nibe.connection.nibegw import NibeGW
9 from nibe.exceptions import (
10  AddressInUseException,
11  CoilNotFoundException,
12  CoilWriteSendException,
13  ReadException,
14  ReadSendException,
15  WriteException,
16 )
17 from nibe.heatpump import HeatPump, Model
18 import voluptuous as vol
19 import yarl
20 
21 from homeassistant.config_entries import ConfigFlow, ConfigFlowResult
22 from homeassistant.const import CONF_IP_ADDRESS, CONF_MODEL
23 from homeassistant.core import HomeAssistant
24 from homeassistant.helpers import selector
25 
26 from .const import (
27  CONF_CONNECTION_TYPE,
28  CONF_CONNECTION_TYPE_MODBUS,
29  CONF_CONNECTION_TYPE_NIBEGW,
30  CONF_LISTENING_PORT,
31  CONF_MODBUS_UNIT,
32  CONF_MODBUS_URL,
33  CONF_REMOTE_READ_PORT,
34  CONF_REMOTE_WRITE_PORT,
35  CONF_WORD_SWAP,
36  DOMAIN,
37  LOGGER,
38 )
39 
40 PORT_SELECTOR = vol.All(
41  selector.NumberSelector(
42  selector.NumberSelectorConfig(
43  min=1, step=1, max=65535, mode=selector.NumberSelectorMode.BOX
44  )
45  ),
46  vol.Coerce(int),
47 )
48 
49 STEP_NIBEGW_DATA_SCHEMA = vol.Schema(
50  {
51  vol.Required(CONF_MODEL): vol.In(list(Model.__members__)),
52  vol.Required(CONF_IP_ADDRESS): selector.TextSelector(),
53  vol.Required(CONF_LISTENING_PORT, default=9999): PORT_SELECTOR,
54  vol.Required(CONF_REMOTE_READ_PORT, default=9999): PORT_SELECTOR,
55  vol.Required(CONF_REMOTE_WRITE_PORT, default=10000): PORT_SELECTOR,
56  }
57 )
58 
59 
60 STEP_MODBUS_DATA_SCHEMA = vol.Schema(
61  {
62  vol.Required(CONF_MODEL): vol.In(list(Model.__members__)),
63  vol.Required(CONF_MODBUS_URL): selector.TextSelector(),
64  vol.Required(CONF_MODBUS_UNIT, default=0): vol.All(
65  selector.NumberSelector(
66  selector.NumberSelectorConfig(
67  min=0, step=1, mode=selector.NumberSelectorMode.BOX
68  )
69  ),
70  vol.Coerce(int),
71  ),
72  }
73 )
74 
75 
76 class FieldError(Exception):
77  """Field with invalid data."""
78 
79  def __init__(self, message: str, field: str, error: str) -> None:
80  """Set up error."""
81  super().__init__(message)
82  self.fieldfield = field
83  self.errorerror = error
84 
85 
87  hass: HomeAssistant, data: dict[str, Any]
88 ) -> tuple[str, dict[str, Any]]:
89  """Validate the user input allows us to connect."""
90 
91  heatpump = HeatPump(Model[data[CONF_MODEL]])
92  heatpump.word_swap = True
93  await heatpump.initialize()
94 
95  connection = NibeGW(
96  heatpump,
97  data[CONF_IP_ADDRESS],
98  data[CONF_REMOTE_READ_PORT],
99  data[CONF_REMOTE_WRITE_PORT],
100  listening_port=data[CONF_LISTENING_PORT],
101  )
102 
103  try:
104  await connection.start()
105  except AddressInUseException as exception:
106  raise FieldError(
107  "Address already in use", "listening_port", "address_in_use"
108  ) from exception
109 
110  try:
111  await connection.verify_connectivity()
112  except (ReadSendException, CoilWriteSendException) as exception:
113  raise FieldError(str(exception), CONF_IP_ADDRESS, "address") from exception
114  except CoilNotFoundException as exception:
115  raise FieldError("Coils not found", "base", "model") from exception
116  except ReadException as exception:
117  raise FieldError("Timeout on read from pump", "base", "read") from exception
118  except WriteException as exception:
119  raise FieldError("Timeout on writing to pump", "base", "write") from exception
120  finally:
121  await connection.stop()
122 
123  return f"{data[CONF_MODEL]} at {data[CONF_IP_ADDRESS]}", {
124  **data,
125  CONF_WORD_SWAP: heatpump.word_swap,
126  CONF_CONNECTION_TYPE: CONF_CONNECTION_TYPE_NIBEGW,
127  }
128 
129 
131  hass: HomeAssistant, data: dict[str, Any]
132 ) -> tuple[str, dict[str, Any]]:
133  """Validate the user input allows us to connect."""
134 
135  heatpump = HeatPump(Model[data[CONF_MODEL]])
136  await heatpump.initialize()
137 
138  try:
139  connection = Modbus(
140  heatpump,
141  data[CONF_MODBUS_URL],
142  data[CONF_MODBUS_UNIT],
143  )
144  except ValueError as exc:
145  raise FieldError("Not a valid modbus url", CONF_MODBUS_URL, "url") from exc
146 
147  await connection.start()
148 
149  try:
150  await connection.verify_connectivity()
151  except (ReadSendException, CoilWriteSendException) as exception:
152  raise FieldError(str(exception), CONF_MODBUS_URL, "address") from exception
153  except CoilNotFoundException as exception:
154  raise FieldError("Coils not found", "base", "model") from exception
155  except ReadException as exception:
156  raise FieldError("Timeout on read from pump", "base", "read") from exception
157  except WriteException as exception:
158  raise FieldError("Timeout on writing to pump", "base", "write") from exception
159  finally:
160  await connection.stop()
161 
162  host = yarl.URL(data[CONF_MODBUS_URL]).host
163  return f"{data[CONF_MODEL]} at {host}", {
164  **data,
165  CONF_CONNECTION_TYPE: CONF_CONNECTION_TYPE_MODBUS,
166  }
167 
168 
169 class NibeHeatPumpConfigFlow(ConfigFlow, domain=DOMAIN):
170  """Handle a config flow for Nibe Heat Pump."""
171 
172  VERSION = 1
173 
174  async def async_step_user(
175  self, user_input: dict[str, Any] | None = None
176  ) -> ConfigFlowResult:
177  """Handle the initial step."""
178  return self.async_show_menuasync_show_menu(step_id="user", menu_options=["modbus", "nibegw"])
179 
180  async def async_step_modbus(
181  self, user_input: dict[str, Any] | None = None
182  ) -> ConfigFlowResult:
183  """Handle the modbus step."""
184  if user_input is None:
185  return self.async_show_formasync_show_formasync_show_form(
186  step_id="modbus", data_schema=STEP_MODBUS_DATA_SCHEMA
187  )
188 
189  errors = {}
190 
191  try:
192  title, data = await validate_modbus_input(self.hass, user_input)
193  except FieldError as exception:
194  LOGGER.debug("Validation error %s", exception)
195  errors[exception.field] = exception.error
196  except Exception: # noqa: BLE001
197  LOGGER.exception("Unexpected exception")
198  errors["base"] = "unknown"
199  else:
200  return self.async_create_entryasync_create_entryasync_create_entry(title=title, data=data)
201 
202  return self.async_show_formasync_show_formasync_show_form(
203  step_id="modbus", data_schema=STEP_MODBUS_DATA_SCHEMA, errors=errors
204  )
205 
206  async def async_step_nibegw(
207  self, user_input: dict[str, Any] | None = None
208  ) -> ConfigFlowResult:
209  """Handle the nibegw step."""
210  if user_input is None:
211  return self.async_show_formasync_show_formasync_show_form(
212  step_id="nibegw", data_schema=STEP_NIBEGW_DATA_SCHEMA
213  )
214 
215  errors = {}
216 
217  try:
218  title, data = await validate_nibegw_input(self.hass, user_input)
219  except FieldError as exception:
220  LOGGER.exception("Validation error")
221  errors[exception.field] = exception.error
222  except Exception: # noqa: BLE001
223  LOGGER.exception("Unexpected exception")
224  errors["base"] = "unknown"
225  else:
226  return self.async_create_entryasync_create_entryasync_create_entry(title=title, data=data)
227 
228  return self.async_show_formasync_show_formasync_show_form(
229  step_id="nibegw", data_schema=STEP_NIBEGW_DATA_SCHEMA, errors=errors
230  )
None __init__(self, str message, str field, str error)
Definition: config_flow.py:79
ConfigFlowResult async_step_modbus(self, dict[str, Any]|None user_input=None)
Definition: config_flow.py:182
ConfigFlowResult async_step_nibegw(self, dict[str, Any]|None user_input=None)
Definition: config_flow.py:208
ConfigFlowResult async_step_user(self, dict[str, Any]|None user_input=None)
Definition: config_flow.py:176
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_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_show_menu(self, *str|None step_id=None, Container[str] menu_options, Mapping[str, str]|None description_placeholders=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)
tuple[str, dict[str, Any]] validate_nibegw_input(HomeAssistant hass, dict[str, Any] data)
Definition: config_flow.py:88
tuple[str, dict[str, Any]] validate_modbus_input(HomeAssistant hass, dict[str, Any] data)
Definition: config_flow.py:132