def test_human_readable_device_name(): """Test human readable device name includes the passed data.""" name = usb.human_readable_device_name( "/dev/null", "612020FD", "Silicon Labs", "HubZ Smart Home Controller - HubZ Z-Wave Com Port", "10C4", "8A2A", ) assert "/dev/null" in name assert "612020FD" in name assert "Silicon Labs" in name assert "HubZ Smart Home Controller - HubZ Z-Wave Com Port"[:26] in name assert "10C4" in name assert "8A2A" in name name = usb.human_readable_device_name( "/dev/null", "612020FD", "Silicon Labs", None, "10C4", "8A2A", ) assert "/dev/null" in name assert "612020FD" in name assert "Silicon Labs" in name assert "10C4" in name assert "8A2A" in name
async def test_flow_user_error(hass: HomeAssistant): """Test user initialized flow with unreachable device.""" port = com_port() port_select = usb.human_readable_device_name( port.device, port.serial_number, port.manufacturer, port.description, port.vid, port.pid, ) with patch_config_flow_modem() as modemmock: modemmock.side_effect = phone_modem.exceptions.SerialError result = await hass.config_entries.flow.async_init( DOMAIN, context={CONF_SOURCE: SOURCE_USER}, data={CONF_DEVICE: port_select}) assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {"base": "cannot_connect"} modemmock.side_effect = None result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={CONF_DEVICE: port_select}, ) assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["data"] == {CONF_DEVICE: port.device}
async def test_flow_user(hass: HomeAssistant): """Test user initialized flow.""" port = com_port() port_select = usb.human_readable_device_name( port.device, port.serial_number, port.manufacturer, port.description, port.vid, port.pid, ) with patch_config_flow_modem(), _patch_setup(): result = await hass.config_entries.flow.async_init( DOMAIN, context={CONF_SOURCE: SOURCE_USER}, data={CONF_DEVICE: port_select}, ) assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["data"] == {CONF_DEVICE: port.device} result = await hass.config_entries.flow.async_init( DOMAIN, context={CONF_SOURCE: SOURCE_USER}, data={CONF_DEVICE: port_select}, ) assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "no_devices_found"
def list_ports_as_str( serial_ports: list[ListPortInfo], no_usb_option: bool = True ) -> list[str]: """ Represent currently available serial ports as string. Adds option to not use usb on top of the list, option to use manual path or refresh list at the end. """ ports_as_string: list[str] = [] if no_usb_option: ports_as_string.append(DONT_USE_USB) for port in serial_ports: ports_as_string.append( usb.human_readable_device_name( port.device, port.serial_number, port.manufacturer, port.description, f"{hex(port.vid)[2:]:0>4}".upper() if port.vid else None, f"{hex(port.pid)[2:]:0>4}".upper() if port.pid else None, ) ) ports_as_string.append(MANUAL_PATH) ports_as_string.append(REFRESH_LIST) return ports_as_string
async def test_successful_login_with_usb( crownstone_setup: MockFixture, pyserial_comports_none_types: MockFixture, usb_path: MockFixture, hass: HomeAssistant, ): """Test flow with correct login and usb configuration.""" entry_data_with_usb = create_mocked_entry_data_conf( email="*****@*****.**", password="******", ) entry_options_with_usb = create_mocked_entry_options_conf( usb_path="/dev/serial/by-id/crownstone-usb", usb_sphere="sphere_id_1", ) result = await start_config_flow( hass, get_mocked_crownstone_cloud(create_mocked_spheres(2)) ) # should show usb form assert result["type"] == data_entry_flow.RESULT_TYPE_FORM assert result["step_id"] == "usb_config" assert pyserial_comports_none_types.call_count == 1 # create a mocked port which should be in # the list returned from list_ports_as_str, from .helpers port = get_mocked_com_port_none_types() port_select = usb.human_readable_device_name( port.device, port.serial_number, port.manufacturer, port.description, port.vid, port.pid, ) # select a port from the list result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={CONF_USB_PATH: port_select} ) assert result["type"] == data_entry_flow.RESULT_TYPE_FORM assert result["step_id"] == "usb_sphere_config" assert pyserial_comports_none_types.call_count == 2 assert usb_path.call_count == 1 # select a sphere result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={CONF_USB_SPHERE: "sphere_name_1"} ) assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY assert result["data"] == entry_data_with_usb assert result["options"] == entry_options_with_usb assert crownstone_setup.call_count == 1
def get_usb_ports() -> dict[str, str]: """Return a dict of USB ports and their friendly names.""" ports = list_ports.comports() port_descriptions = {} for port in ports: usb_device = usb.usb_device_from_port(port) dev_path = usb.get_serial_by_id(usb_device.device) human_name = usb.human_readable_device_name( dev_path, usb_device.serial_number, usb_device.manufacturer, usb_device.description, usb_device.vid, usb_device.pid, ) port_descriptions[dev_path] = human_name return port_descriptions
async def async_step_usb(self, discovery_info: dict[str, str]) -> FlowResult: """Handle USB Discovery.""" if not is_hassio(self.hass): return self.async_abort(reason="discovery_requires_supervisor") if self._async_current_entries(): return self.async_abort(reason="already_configured") if self._async_in_progress(): return self.async_abort(reason="already_in_progress") vid = discovery_info["vid"] pid = discovery_info["pid"] serial_number = discovery_info["serial_number"] device = discovery_info["device"] manufacturer = discovery_info["manufacturer"] description = discovery_info["description"] # The Nortek sticks are a special case since they # have a Z-Wave and a Zigbee radio. We need to reject # the Zigbee radio. if vid == "10C4" and pid == "8A2A" and "Z-Wave" not in description: return self.async_abort(reason="not_zwave_device") # Zooz uses this vid/pid, but so do 2652 sticks if vid == "10C4" and pid == "EA60" and "2652" in description: return self.async_abort(reason="not_zwave_device") addon_info = await self._async_get_addon_info() if addon_info.state not in (AddonState.NOT_INSTALLED, AddonState.NOT_RUNNING): return self.async_abort(reason="already_configured") await self.async_set_unique_id( f"{vid}:{pid}_{serial_number}_{manufacturer}_{description}") self._abort_if_unique_id_configured() dev_path = await self.hass.async_add_executor_job( usb.get_serial_by_id, device) self.usb_path = dev_path self._title = usb.human_readable_device_name( dev_path, serial_number, manufacturer, description, vid, pid, ) self.context["title_placeholders"] = {CONF_NAME: self._title} return await self.async_step_usb_confirm()
async def async_step_usb(self, discovery_info: usb.UsbServiceInfo) -> FlowResult: """Handle USB Discovery.""" if not is_hassio(self.hass): return self.async_abort(reason="discovery_requires_supervisor") if self._async_current_entries(): return self.async_abort(reason="already_configured") if self._async_in_progress(): return self.async_abort(reason="already_in_progress") vid = discovery_info.vid pid = discovery_info.pid serial_number = discovery_info.serial_number device = discovery_info.device manufacturer = discovery_info.manufacturer description = discovery_info.description # Zooz uses this vid/pid, but so do 2652 sticks if vid == "10C4" and pid == "EA60" and description and "2652" in description: return self.async_abort(reason="not_zwave_device") addon_info = await self._async_get_addon_info() if addon_info.state not in (AddonState.NOT_INSTALLED, AddonState.NOT_RUNNING): return self.async_abort(reason="already_configured") await self.async_set_unique_id( f"{vid}:{pid}_{serial_number}_{manufacturer}_{description}") self._abort_if_unique_id_configured() dev_path = await self.hass.async_add_executor_job( usb.get_serial_by_id, device) self.usb_path = dev_path self._title = usb.human_readable_device_name( dev_path, serial_number, manufacturer, description, vid, pid, ) self.context["title_placeholders"] = { CONF_NAME: self._title.split(" - ")[0].strip() } return await self.async_step_usb_confirm()
async def async_step_user( self, user_input: dict[str, Any] | None = None ) -> FlowResult: """Handle a flow initiated by the user.""" errors: dict[str, str] | None = {} if self._async_in_progress(): return self.async_abort(reason="already_in_progress") ports = await self.hass.async_add_executor_job(serial.tools.list_ports.comports) existing_devices = [ entry.data[CONF_DEVICE] for entry in self._async_current_entries() ] unused_ports = [ usb.human_readable_device_name( port.device, port.serial_number, port.manufacturer, port.description, port.vid, port.pid, ) for port in ports if port.device not in existing_devices ] if not unused_ports: return self.async_abort(reason="no_devices_found") if user_input is not None: port = ports[unused_ports.index(str(user_input.get(CONF_DEVICE)))] dev_path = await self.hass.async_add_executor_job( usb.get_serial_by_id, port.device ) errors = await self.validate_device_errors( dev_path=dev_path, unique_id=_generate_unique_id(port) ) if errors is None: return self.async_create_entry( title=user_input.get(CONF_NAME, DEFAULT_NAME), data={CONF_DEVICE: dev_path}, ) user_input = user_input or {} schema = vol.Schema({vol.Required(CONF_DEVICE): vol.In(unused_ports)}) return self.async_show_form(step_id="user", data_schema=schema, errors=errors)
async def async_step_usb(self, discovery_info: usb.UsbServiceInfo) -> FlowResult: """Handle USB discovery.""" if self._async_current_entries(): return self.async_abort(reason="single_instance_allowed") dev_path = await self.hass.async_add_executor_job( usb.get_serial_by_id, discovery_info.device) self._device_path = dev_path self._device_name = usb.human_readable_device_name( dev_path, discovery_info.serial_number, discovery_info.manufacturer, discovery_info.description, discovery_info.vid, discovery_info.pid, ) self._set_confirm_only() self.context["title_placeholders"] = {CONF_NAME: self._device_name} await self.async_set_unique_id( config_entries.DEFAULT_DISCOVERY_UNIQUE_ID) return await self.async_step_confirm_usb()
class ZhaFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): """Handle a config flow.""" VERSION = 3 def __init__(self): """Initialize flow instance.""" self._device_path = None self._radio_type = None self._title = None async def async_step_user(self, user_input=None): """Handle a zha config flow start.""" if self._async_current_entries(): return self.async_abort(reason="single_instance_allowed") 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 ] if not list_of_ports: return await self.async_step_pick_radio() list_of_ports.append(CONF_MANUAL_PATH) if user_input is not None: user_selection = user_input[CONF_DEVICE_PATH] if user_selection == CONF_MANUAL_PATH: return await self.async_step_pick_radio() port = ports[list_of_ports.index(user_selection)] dev_path = await self.hass.async_add_executor_job( usb.get_serial_by_id, port.device) auto_detected_data = await detect_radios(dev_path) if auto_detected_data is not None: title = f"{port.description}, s/n: {port.serial_number or 'n/a'}" title += f" - {port.manufacturer}" if port.manufacturer else "" return self.async_create_entry( title=title, data=auto_detected_data, ) # did not detect anything self._device_path = dev_path return await self.async_step_pick_radio() schema = vol.Schema( {vol.Required(CONF_DEVICE_PATH): vol.In(list_of_ports)}) return self.async_show_form(step_id="user", data_schema=schema) async def async_step_pick_radio(self, user_input=None): """Select radio type.""" if user_input is not None: self._radio_type = RadioType.get_by_description( user_input[CONF_RADIO_TYPE]) return await self.async_step_port_config() schema = { vol.Required(CONF_RADIO_TYPE): vol.In(sorted(RadioType.list())) } return self.async_show_form( step_id="pick_radio", data_schema=vol.Schema(schema), ) async def async_step_usb(self, discovery_info: usb.UsbServiceInfo) -> FlowResult: """Handle usb discovery.""" vid = discovery_info.vid pid = discovery_info.pid serial_number = discovery_info.serial_number device = discovery_info.device manufacturer = discovery_info.manufacturer description = discovery_info.description dev_path = await self.hass.async_add_executor_job( usb.get_serial_by_id, device) unique_id = f"{vid}:{pid}_{serial_number}_{manufacturer}_{description}" if current_entry := await self.async_set_unique_id(unique_id): self._abort_if_unique_id_configured( updates={ CONF_DEVICE: { **current_entry.data.get(CONF_DEVICE, {}), CONF_DEVICE_PATH: dev_path, }, }) # Check if already configured if self._async_current_entries(): return self.async_abort(reason="single_instance_allowed") # If they already have a discovery for deconz # we ignore the usb discovery as they probably # want to use it there instead if self.hass.config_entries.flow.async_progress_by_handler( DECONZ_DOMAIN): return self.async_abort(reason="not_zha_device") for entry in self.hass.config_entries.async_entries(DECONZ_DOMAIN): if entry.source != config_entries.SOURCE_IGNORE: return self.async_abort(reason="not_zha_device") self._device_path = dev_path self._title = usb.human_readable_device_name( dev_path, serial_number, manufacturer, description, vid, pid, ) self._set_confirm_only() self.context["title_placeholders"] = {CONF_NAME: self._title} return await self.async_step_confirm()
async def test_options_flow_setup_usb(serial_mock: MagicMock, hass: HomeAssistant): """Test options flow init.""" configured_entry_data = create_mocked_entry_data_conf( email="*****@*****.**", password="******", ) configured_entry_options = create_mocked_entry_options_conf( usb_path=None, usb_sphere=None, ) # create mocked entry entry = MockConfigEntry( domain=DOMAIN, data=configured_entry_data, options=configured_entry_options, unique_id="account_id", ) entry.add_to_hass(hass) result = await start_options_flow( hass, entry.entry_id, get_mocked_crownstone_cloud(create_mocked_spheres(2))) assert result["type"] == data_entry_flow.RESULT_TYPE_FORM assert result["step_id"] == "init" schema = result["data_schema"].schema for schema_key in schema: if schema_key == CONF_USE_USB_OPTION: assert not schema_key.default() # USB is not set up, so this should not be in the options assert CONF_USB_SPHERE_OPTION not in schema result = await hass.config_entries.options.async_configure( result["flow_id"], user_input={CONF_USE_USB_OPTION: True}) assert result["type"] == data_entry_flow.RESULT_TYPE_FORM assert result["step_id"] == "usb_config_option" # create a mocked port port = get_mocked_com_port() port_select = usb.human_readable_device_name( port.device, port.serial_number, port.manufacturer, port.description, f"{hex(port.vid)[2:]:0>4}".upper(), f"{hex(port.pid)[2:]:0>4}".upper(), ) # select a port from the list result = await hass.config_entries.options.async_configure( result["flow_id"], user_input={CONF_USB_PATH: port_select}) assert result["type"] == data_entry_flow.RESULT_TYPE_FORM assert result["step_id"] == "usb_sphere_config_option" assert serial_mock.call_count == 1 # select a sphere result = await hass.config_entries.options.async_configure( result["flow_id"], user_input={CONF_USB_SPHERE: "sphere_name_1"}) assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY assert result["data"] == create_mocked_entry_options_conf( usb_path="/dev/serial/by-id/crownstone-usb", usb_sphere="sphere_id_1")