Home Assistant Unofficial Reference 2024.12.1
config_flow.py
Go to the documentation of this file.
1 """Config flow for Rainforest RAVEn devices."""
2 
3 from __future__ import annotations
4 
5 import asyncio
6 from typing import Any
7 
8 from aioraven.data import MeterType
9 from aioraven.device import RAVEnConnectionError
10 from aioraven.serial import RAVEnSerialDevice
11 import serial.tools.list_ports
12 from serial.tools.list_ports_common import ListPortInfo
13 import voluptuous as vol
14 
15 from homeassistant.components import usb
16 from homeassistant.config_entries import ConfigFlow, ConfigFlowResult
17 from homeassistant.const import CONF_DEVICE, CONF_MAC, CONF_NAME
19  SelectSelector,
20  SelectSelectorConfig,
21  SelectSelectorMode,
22 )
23 
24 from .const import DEFAULT_NAME, DOMAIN
25 
26 
27 def _format_id(value: str | int) -> str:
28  if isinstance(value, str):
29  return value
30  return f"{value or 0:04X}"
31 
32 
33 def _generate_unique_id(info: ListPortInfo | usb.UsbServiceInfo) -> str:
34  """Generate unique id from usb attributes."""
35  return (
36  f"{_format_id(info.vid)}:{_format_id(info.pid)}_{info.serial_number}"
37  f"_{info.manufacturer}_{info.description}"
38  )
39 
40 
41 class RainforestRavenConfigFlow(ConfigFlow, domain=DOMAIN):
42  """Handle a config flow for Rainforest RAVEn devices."""
43 
44  def __init__(self) -> None:
45  """Set up flow instance."""
46  self._dev_path_dev_path: str | None = None
47  self._meter_macs: set[str] = set()
48 
49  async def _validate_device(self, dev_path: str) -> None:
50  self._abort_if_unique_id_configured_abort_if_unique_id_configured(updates={CONF_DEVICE: dev_path})
51  async with (
52  asyncio.timeout(5),
53  RAVEnSerialDevice(dev_path) as raven_device,
54  ):
55  await raven_device.synchronize()
56  meters = await raven_device.get_meter_list()
57  if meters:
58  for meter in meters.meter_mac_ids or ():
59  meter_info = await raven_device.get_meter_info(meter=meter)
60  if meter_info and (
61  meter_info.meter_type is None
62  or meter_info.meter_type == MeterType.ELECTRIC
63  ):
64  self._meter_macs.add(meter.hex())
65  self._dev_path_dev_path = dev_path
66 
67  async def async_step_meters(
68  self, user_input: dict[str, Any] | None = None
69  ) -> ConfigFlowResult:
70  """Connect to device and discover meters."""
71  errors: dict[str, str] = {}
72  if user_input is not None:
73  meter_macs = []
74  for raw_mac in user_input.get(CONF_MAC, ()):
75  mac = bytes.fromhex(raw_mac).hex()
76  if mac not in meter_macs:
77  meter_macs.append(mac)
78  if meter_macs and not errors:
79  return self.async_create_entryasync_create_entryasync_create_entry(
80  title=user_input.get(CONF_NAME, DEFAULT_NAME),
81  data={
82  CONF_DEVICE: self._dev_path_dev_path,
83  CONF_MAC: meter_macs,
84  },
85  )
86 
87  schema = vol.Schema(
88  {
89  vol.Required(CONF_MAC): SelectSelector(
91  options=sorted(self._meter_macs),
92  mode=SelectSelectorMode.DROPDOWN,
93  multiple=True,
94  translation_key=CONF_MAC,
95  )
96  ),
97  }
98  )
99  return self.async_show_formasync_show_formasync_show_form(step_id="meters", data_schema=schema, errors=errors)
100 
101  async def async_step_usb(
102  self, discovery_info: usb.UsbServiceInfo
103  ) -> ConfigFlowResult:
104  """Handle USB Discovery."""
105  device = discovery_info.device
106  dev_path = await self.hass.async_add_executor_job(usb.get_serial_by_id, device)
107  unique_id = _generate_unique_id(discovery_info)
108  await self.async_set_unique_idasync_set_unique_id(unique_id)
109  try:
110  await self._validate_device_validate_device(dev_path)
111  except TimeoutError:
112  return self.async_abortasync_abortasync_abort(reason="timeout_connect")
113  except RAVEnConnectionError:
114  return self.async_abortasync_abortasync_abort(reason="cannot_connect")
115  return await self.async_step_metersasync_step_meters()
116 
117  async def async_step_user(
118  self, user_input: dict[str, Any] | None = None
119  ) -> ConfigFlowResult:
120  """Handle a flow initiated by the user."""
121  if self._async_in_progress_async_in_progress():
122  return self.async_abortasync_abortasync_abort(reason="already_in_progress")
123  ports = await self.hass.async_add_executor_job(serial.tools.list_ports.comports)
124  existing_devices = [
125  entry.data[CONF_DEVICE] for entry in self._async_current_entries_async_current_entries()
126  ]
127  unused_ports = [
128  usb.human_readable_device_name(
129  port.device,
130  port.serial_number,
131  port.manufacturer,
132  port.description,
133  port.vid,
134  port.pid,
135  )
136  for port in ports
137  if port.device not in existing_devices
138  ]
139  if not unused_ports:
140  return self.async_abortasync_abortasync_abort(reason="no_devices_found")
141 
142  errors = {}
143  if user_input is not None and user_input.get(CONF_DEVICE, "").strip():
144  port = ports[unused_ports.index(str(user_input[CONF_DEVICE]))]
145  dev_path = await self.hass.async_add_executor_job(
146  usb.get_serial_by_id, port.device
147  )
148  unique_id = _generate_unique_id(port)
149  await self.async_set_unique_idasync_set_unique_id(unique_id)
150  try:
151  await self._validate_device_validate_device(dev_path)
152  except TimeoutError:
153  errors[CONF_DEVICE] = "timeout_connect"
154  except RAVEnConnectionError:
155  errors[CONF_DEVICE] = "cannot_connect"
156  else:
157  return await self.async_step_metersasync_step_meters()
158 
159  schema = vol.Schema({vol.Required(CONF_DEVICE): vol.In(unused_ports)})
160  return self.async_show_formasync_show_formasync_show_form(step_id="user", data_schema=schema, errors=errors)
ConfigFlowResult async_step_meters(self, dict[str, Any]|None user_input=None)
Definition: config_flow.py:69
ConfigFlowResult async_step_user(self, dict[str, Any]|None user_input=None)
Definition: config_flow.py:119
ConfigFlowResult async_step_usb(self, usb.UsbServiceInfo discovery_info)
Definition: config_flow.py:103
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)
list[ConfigFlowResult] _async_in_progress(self, bool include_uninitialized=False, dict[str, Any]|None match_context=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)
bool add(self, _T matcher)
Definition: match.py:185
str _generate_unique_id(ListPortInfo|usb.UsbServiceInfo info)
Definition: config_flow.py:33