-
-
Notifications
You must be signed in to change notification settings - Fork 37.1k
Add support for Plugwise USB stick #44186
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
11d3bbe
14f33fb
3817658
fb24052
c8f72c8
a4c0b8a
01092e5
cdf7c08
1aa5db8
0154918
8d5fceb
c7fd465
b9f1d0f
60e11d5
6260e66
2c83a63
935f014
16c1c12
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,8 +1,19 @@ | ||
| """Config flow for Plugwise integration.""" | ||
| import logging | ||
|
|
||
| from plugwise.exceptions import InvalidAuthentication, PlugwiseException | ||
| import os | ||
| from typing import Dict | ||
|
|
||
| import plugwise | ||
| from plugwise.exceptions import ( | ||
| InvalidAuthentication, | ||
| NetworkDown, | ||
| PlugwiseException, | ||
| PortError, | ||
| StickInitError, | ||
| TimeoutException, | ||
| ) | ||
| from plugwise.smile import Smile | ||
| import serial.tools.list_ports | ||
| import voluptuous as vol | ||
|
|
||
| from homeassistant import config_entries, core, exceptions | ||
|
|
@@ -20,14 +31,77 @@ | |
| from homeassistant.helpers.typing import DiscoveryInfoType | ||
|
|
||
| from .const import ( # pylint:disable=unused-import | ||
| CONF_USB_PATH, | ||
| DEFAULT_PORT, | ||
| DEFAULT_SCAN_INTERVAL, | ||
| DOMAIN, | ||
| FLOW_NET, | ||
| FLOW_TYPE, | ||
| FLOW_USB, | ||
| PW_TYPE, | ||
| SMILE, | ||
| STICK, | ||
| STRETCH, | ||
| ZEROCONF_MAP, | ||
| ) | ||
|
|
||
| _LOGGER = logging.getLogger(__name__) | ||
|
|
||
| CONF_MANUAL_PATH = "Enter Manually" | ||
|
|
||
| CONNECTION_SCHEMA = vol.Schema( | ||
| { | ||
| vol.Required(FLOW_TYPE, default=FLOW_NET): vol.In( | ||
| {FLOW_NET: f"Network: {SMILE} / {STRETCH}", FLOW_USB: "USB: Stick"} | ||
| ), | ||
| }, | ||
| ) | ||
|
|
||
|
|
||
| @callback | ||
| def plugwise_stick_entries(hass): | ||
| """Return existing connections for Plugwise USB-stick domain.""" | ||
| sticks = [] | ||
| for entry in hass.config_entries.async_entries(DOMAIN): | ||
| if entry.data.get(PW_TYPE) == STICK: | ||
| sticks.append(entry.data.get(CONF_USB_PATH)) | ||
| return sticks | ||
|
|
||
|
|
||
| async def validate_usb_connection(self, device_path=None) -> Dict[str, str]: | ||
| """Test if device_path is a real Plugwise USB-Stick.""" | ||
| errors = {} | ||
| # Avoid creating a 2nd connection to an already configured stick | ||
| if device_path in plugwise_stick_entries(self): | ||
| errors[CONF_BASE] = "already_configured" | ||
| return errors, None | ||
|
|
||
| stick = await self.async_add_executor_job(plugwise.stick, device_path) | ||
| try: | ||
| await self.async_add_executor_job(stick.connect) | ||
| await self.async_add_executor_job(stick.initialize_stick) | ||
| await self.async_add_executor_job(stick.disconnect) | ||
| except PortError: | ||
| errors[CONF_BASE] = "cannot_connect" | ||
| except StickInitError: | ||
| errors[CONF_BASE] = "stick_init" | ||
| except NetworkDown: | ||
| errors[CONF_BASE] = "network_down" | ||
| except TimeoutException: | ||
| errors[CONF_BASE] = "network_timeout" | ||
| return errors, stick | ||
|
Comment on lines
+79
to
+92
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Please avoid all the jumps into the executor by wrapping them up in one function.
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @bdraco, first a question: why should this be avoided? And, combining them in one function is not practical: there are 4 functions, we call use 3 of them here, and in usb.py we use all four of them but in a different combination.
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Switching from async to sync context is expensive. We should try to avoid doing it multiple times when we can do one call to the executor. |
||
|
|
||
|
|
||
| def get_serial_by_id(dev_path: str) -> str: | ||
| """Return a /dev/serial/by-id match for given device if available.""" | ||
| by_id = "/dev/serial/by-id" | ||
| if not os.path.isdir(by_id): | ||
| return dev_path | ||
| for path in (entry.path for entry in os.scandir(by_id) if entry.is_symlink()): | ||
| if os.path.realpath(path) == dev_path: | ||
| return path | ||
| return dev_path | ||
|
|
||
|
|
||
| def _base_gw_schema(discovery_info): | ||
| """Generate base schema for gateways.""" | ||
|
|
@@ -108,7 +182,7 @@ async def async_step_zeroconf(self, discovery_info: DiscoveryInfoType): | |
| CONF_PORT: discovery_info.get(CONF_PORT, DEFAULT_PORT), | ||
| CONF_NAME: _name, | ||
| } | ||
| return await self.async_step_user() | ||
| return await self.async_step_user({FLOW_TYPE: FLOW_NET}) | ||
|
|
||
| async def async_step_user_gateway(self, user_input=None): | ||
| """Handle the initial step for gateways.""" | ||
|
|
@@ -148,13 +222,79 @@ async def async_step_user_gateway(self, user_input=None): | |
| errors=errors or {}, | ||
| ) | ||
|
|
||
| # PLACEHOLDER USB async_step_user_usb and async_step_user_usb_manual_paht | ||
| async def async_step_user_usb(self, user_input=None): | ||
| """Step when user initializes a integration.""" | ||
| errors = {} | ||
| ports = await self.hass.async_add_executor_job(serial.tools.list_ports.comports) | ||
| list_of_ports = [ | ||
| f"{p}, s/n: {p.serial_number or 'n/a'}" | ||
| + (f" - {p.manufacturer}" if p.manufacturer else "") | ||
| for p in ports | ||
| ] | ||
| list_of_ports.append(CONF_MANUAL_PATH) | ||
|
|
||
| if user_input is not None: | ||
| user_selection = user_input[CONF_USB_PATH] | ||
| if user_selection == CONF_MANUAL_PATH: | ||
| return await self.async_step_manual_path() | ||
| port = ports[list_of_ports.index(user_selection)] | ||
| device_path = await self.hass.async_add_executor_job( | ||
| get_serial_by_id, port.device | ||
| ) | ||
| errors, stick = await validate_usb_connection(self.hass, device_path) | ||
| if not errors: | ||
| await self.async_set_unique_id(stick.get_mac_stick()) | ||
| return self.async_create_entry( | ||
| title="Stick", data={CONF_USB_PATH: device_path, PW_TYPE: STICK} | ||
| ) | ||
| return self.async_show_form( | ||
| step_id="user_usb", | ||
| data_schema=vol.Schema( | ||
| {vol.Required(CONF_USB_PATH): vol.In(list_of_ports)} | ||
| ), | ||
| errors=errors, | ||
| ) | ||
|
|
||
| async def async_step_manual_path(self, user_input=None): | ||
| """Step when manual path to device.""" | ||
| errors = {} | ||
| if user_input is not None: | ||
| device_path = await self.hass.async_add_executor_job( | ||
| get_serial_by_id, user_input.get(CONF_USB_PATH) | ||
| ) | ||
| errors, stick = await validate_usb_connection(self.hass, device_path) | ||
| if not errors: | ||
| await self.async_set_unique_id(stick.get_mac_stick()) | ||
| return self.async_create_entry( | ||
| title="Stick", data={CONF_USB_PATH: device_path} | ||
| ) | ||
| return self.async_show_form( | ||
| step_id="manual_path", | ||
| data_schema=vol.Schema( | ||
| { | ||
| vol.Required( | ||
| CONF_USB_PATH, default="/dev/ttyUSB0" or vol.UNDEFINED | ||
| ): str | ||
| } | ||
| ), | ||
| errors=errors, | ||
| ) | ||
|
|
||
| async def async_step_user(self, user_input=None): | ||
| """Handle the initial step.""" | ||
| """Handle the initial step when using network/gateway setups.""" | ||
| errors = {} | ||
| if user_input is not None: | ||
| if user_input[FLOW_TYPE] == FLOW_NET: | ||
| return await self.async_step_user_gateway() | ||
|
|
||
| # PLACEHOLDER USB vs Gateway Logic | ||
| return await self.async_step_user_gateway() | ||
| if user_input[FLOW_TYPE] == FLOW_USB: | ||
| return await self.async_step_user_usb() | ||
|
|
||
| return self.async_show_form( | ||
| step_id="user", | ||
| data_schema=CONNECTION_SCHEMA, | ||
| errors=errors, | ||
| ) | ||
|
|
||
| @staticmethod | ||
| @callback | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is it really necessary to call a class using
async_add_executor_job()?Maybe better to use
from plugwise import stickand then use something like
api_stick = stick(device_path)?