mirror of
https://github.com/home-assistant/core.git
synced 2025-04-26 10:17:51 +00:00
Add zwave_js usb port selection (#76385)
This commit is contained in:
parent
1fe44d0997
commit
a6963e6a38
@ -8,6 +8,7 @@ from typing import Any
|
|||||||
|
|
||||||
import aiohttp
|
import aiohttp
|
||||||
from async_timeout import timeout
|
from async_timeout import timeout
|
||||||
|
from serial.tools import list_ports
|
||||||
import voluptuous as vol
|
import voluptuous as vol
|
||||||
from zwave_js_server.version import VersionInfo, get_server_version
|
from zwave_js_server.version import VersionInfo, get_server_version
|
||||||
|
|
||||||
@ -119,6 +120,30 @@ async def async_get_version_info(hass: HomeAssistant, ws_address: str) -> Versio
|
|||||||
return version_info
|
return version_info
|
||||||
|
|
||||||
|
|
||||||
|
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_get_usb_ports(hass: HomeAssistant) -> dict[str, str]:
|
||||||
|
"""Return a dict of USB ports and their friendly names."""
|
||||||
|
return await hass.async_add_executor_job(get_usb_ports)
|
||||||
|
|
||||||
|
|
||||||
class BaseZwaveJSFlow(FlowHandler):
|
class BaseZwaveJSFlow(FlowHandler):
|
||||||
"""Represent the base config flow for Z-Wave JS."""
|
"""Represent the base config flow for Z-Wave JS."""
|
||||||
|
|
||||||
@ -402,7 +427,9 @@ class ConfigFlow(BaseZwaveJSFlow, config_entries.ConfigFlow, domain=DOMAIN):
|
|||||||
vid,
|
vid,
|
||||||
pid,
|
pid,
|
||||||
)
|
)
|
||||||
self.context["title_placeholders"] = {CONF_NAME: self._title}
|
self.context["title_placeholders"] = {
|
||||||
|
CONF_NAME: self._title.split(" - ")[0].strip()
|
||||||
|
}
|
||||||
return await self.async_step_usb_confirm()
|
return await self.async_step_usb_confirm()
|
||||||
|
|
||||||
async def async_step_usb_confirm(
|
async def async_step_usb_confirm(
|
||||||
@ -579,7 +606,11 @@ class ConfigFlow(BaseZwaveJSFlow, config_entries.ConfigFlow, domain=DOMAIN):
|
|||||||
}
|
}
|
||||||
|
|
||||||
if not self._usb_discovery:
|
if not self._usb_discovery:
|
||||||
schema = {vol.Required(CONF_USB_PATH, default=usb_path): str, **schema}
|
ports = await async_get_usb_ports(self.hass)
|
||||||
|
schema = {
|
||||||
|
vol.Required(CONF_USB_PATH, default=usb_path): vol.In(ports),
|
||||||
|
**schema,
|
||||||
|
}
|
||||||
|
|
||||||
data_schema = vol.Schema(schema)
|
data_schema = vol.Schema(schema)
|
||||||
|
|
||||||
@ -801,9 +832,11 @@ class OptionsFlowHandler(BaseZwaveJSFlow, config_entries.OptionsFlow):
|
|||||||
log_level = addon_config.get(CONF_ADDON_LOG_LEVEL, "info")
|
log_level = addon_config.get(CONF_ADDON_LOG_LEVEL, "info")
|
||||||
emulate_hardware = addon_config.get(CONF_ADDON_EMULATE_HARDWARE, False)
|
emulate_hardware = addon_config.get(CONF_ADDON_EMULATE_HARDWARE, False)
|
||||||
|
|
||||||
|
ports = await async_get_usb_ports(self.hass)
|
||||||
|
|
||||||
data_schema = vol.Schema(
|
data_schema = vol.Schema(
|
||||||
{
|
{
|
||||||
vol.Required(CONF_USB_PATH, default=usb_path): str,
|
vol.Required(CONF_USB_PATH, default=usb_path): vol.In(ports),
|
||||||
vol.Optional(CONF_S0_LEGACY_KEY, default=s0_legacy_key): str,
|
vol.Optional(CONF_S0_LEGACY_KEY, default=s0_legacy_key): str,
|
||||||
vol.Optional(
|
vol.Optional(
|
||||||
CONF_S2_ACCESS_CONTROL_KEY, default=s2_access_control_key
|
CONF_S2_ACCESS_CONTROL_KEY, default=s2_access_control_key
|
||||||
|
@ -3,7 +3,7 @@
|
|||||||
"name": "Z-Wave",
|
"name": "Z-Wave",
|
||||||
"config_flow": true,
|
"config_flow": true,
|
||||||
"documentation": "https://www.home-assistant.io/integrations/zwave_js",
|
"documentation": "https://www.home-assistant.io/integrations/zwave_js",
|
||||||
"requirements": ["zwave-js-server-python==0.40.0"],
|
"requirements": ["pyserial==3.5", "zwave-js-server-python==0.40.0"],
|
||||||
"codeowners": ["@home-assistant/z-wave"],
|
"codeowners": ["@home-assistant/z-wave"],
|
||||||
"dependencies": ["usb", "http", "websocket_api"],
|
"dependencies": ["usb", "http", "websocket_api"],
|
||||||
"iot_class": "local_push",
|
"iot_class": "local_push",
|
||||||
|
@ -1815,6 +1815,7 @@ pyserial-asyncio==0.6
|
|||||||
# homeassistant.components.crownstone
|
# homeassistant.components.crownstone
|
||||||
# homeassistant.components.usb
|
# homeassistant.components.usb
|
||||||
# homeassistant.components.zha
|
# homeassistant.components.zha
|
||||||
|
# homeassistant.components.zwave_js
|
||||||
pyserial==3.5
|
pyserial==3.5
|
||||||
|
|
||||||
# homeassistant.components.sesame
|
# homeassistant.components.sesame
|
||||||
|
@ -1250,6 +1250,7 @@ pyserial-asyncio==0.6
|
|||||||
# homeassistant.components.crownstone
|
# homeassistant.components.crownstone
|
||||||
# homeassistant.components.usb
|
# homeassistant.components.usb
|
||||||
# homeassistant.components.zha
|
# homeassistant.components.zha
|
||||||
|
# homeassistant.components.zwave_js
|
||||||
pyserial==3.5
|
pyserial==3.5
|
||||||
|
|
||||||
# homeassistant.components.sia
|
# homeassistant.components.sia
|
||||||
|
@ -1,9 +1,12 @@
|
|||||||
"""Test the Z-Wave JS config flow."""
|
"""Test the Z-Wave JS config flow."""
|
||||||
import asyncio
|
import asyncio
|
||||||
from unittest.mock import DEFAULT, call, patch
|
from collections.abc import Generator
|
||||||
|
from copy import copy
|
||||||
|
from unittest.mock import DEFAULT, MagicMock, call, patch
|
||||||
|
|
||||||
import aiohttp
|
import aiohttp
|
||||||
import pytest
|
import pytest
|
||||||
|
from serial.tools.list_ports_common import ListPortInfo
|
||||||
from zwave_js_server.version import VersionInfo
|
from zwave_js_server.version import VersionInfo
|
||||||
|
|
||||||
from homeassistant import config_entries
|
from homeassistant import config_entries
|
||||||
@ -134,6 +137,45 @@ def mock_addon_setup_time():
|
|||||||
yield addon_setup_time
|
yield addon_setup_time
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture(name="serial_port")
|
||||||
|
def serial_port_fixture() -> ListPortInfo:
|
||||||
|
"""Return a mock serial port."""
|
||||||
|
port = ListPortInfo("/test", skip_link_detection=True)
|
||||||
|
port.serial_number = "1234"
|
||||||
|
port.manufacturer = "Virtual serial port"
|
||||||
|
port.device = "/test"
|
||||||
|
port.description = "Some serial port"
|
||||||
|
port.pid = 9876
|
||||||
|
port.vid = 5678
|
||||||
|
|
||||||
|
return port
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture(name="mock_list_ports", autouse=True)
|
||||||
|
def mock_list_ports_fixture(serial_port) -> Generator[MagicMock, None, None]:
|
||||||
|
"""Mock list ports."""
|
||||||
|
with patch(
|
||||||
|
"homeassistant.components.zwave_js.config_flow.list_ports.comports"
|
||||||
|
) as mock_list_ports:
|
||||||
|
another_port = copy(serial_port)
|
||||||
|
another_port.device = "/new"
|
||||||
|
another_port.description = "New serial port"
|
||||||
|
another_port.serial_number = "5678"
|
||||||
|
another_port.pid = 8765
|
||||||
|
mock_list_ports.return_value = [serial_port, another_port]
|
||||||
|
yield mock_list_ports
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture(name="mock_usb_serial_by_id", autouse=True)
|
||||||
|
def mock_usb_serial_by_id_fixture() -> Generator[MagicMock, None, None]:
|
||||||
|
"""Mock usb serial by id."""
|
||||||
|
with patch(
|
||||||
|
"homeassistant.components.zwave_js.config_flow.usb.get_serial_by_id"
|
||||||
|
) as mock_usb_serial_by_id:
|
||||||
|
mock_usb_serial_by_id.side_effect = lambda x: x
|
||||||
|
yield mock_usb_serial_by_id
|
||||||
|
|
||||||
|
|
||||||
async def test_manual(hass):
|
async def test_manual(hass):
|
||||||
"""Test we create an entry with manual step."""
|
"""Test we create an entry with manual step."""
|
||||||
|
|
||||||
@ -1397,7 +1439,7 @@ async def test_addon_installed_already_configured(
|
|||||||
result = await hass.config_entries.flow.async_configure(
|
result = await hass.config_entries.flow.async_configure(
|
||||||
result["flow_id"],
|
result["flow_id"],
|
||||||
{
|
{
|
||||||
"usb_path": "/test_new",
|
"usb_path": "/new",
|
||||||
"s0_legacy_key": "new123",
|
"s0_legacy_key": "new123",
|
||||||
"s2_access_control_key": "new456",
|
"s2_access_control_key": "new456",
|
||||||
"s2_authenticated_key": "new789",
|
"s2_authenticated_key": "new789",
|
||||||
@ -1410,7 +1452,7 @@ async def test_addon_installed_already_configured(
|
|||||||
"core_zwave_js",
|
"core_zwave_js",
|
||||||
{
|
{
|
||||||
"options": {
|
"options": {
|
||||||
"device": "/test_new",
|
"device": "/new",
|
||||||
"s0_legacy_key": "new123",
|
"s0_legacy_key": "new123",
|
||||||
"s2_access_control_key": "new456",
|
"s2_access_control_key": "new456",
|
||||||
"s2_authenticated_key": "new789",
|
"s2_authenticated_key": "new789",
|
||||||
@ -1430,7 +1472,7 @@ async def test_addon_installed_already_configured(
|
|||||||
assert result["type"] == "abort"
|
assert result["type"] == "abort"
|
||||||
assert result["reason"] == "already_configured"
|
assert result["reason"] == "already_configured"
|
||||||
assert entry.data["url"] == "ws://host1:3001"
|
assert entry.data["url"] == "ws://host1:3001"
|
||||||
assert entry.data["usb_path"] == "/test_new"
|
assert entry.data["usb_path"] == "/new"
|
||||||
assert entry.data["s0_legacy_key"] == "new123"
|
assert entry.data["s0_legacy_key"] == "new123"
|
||||||
assert entry.data["s2_access_control_key"] == "new456"
|
assert entry.data["s2_access_control_key"] == "new456"
|
||||||
assert entry.data["s2_authenticated_key"] == "new789"
|
assert entry.data["s2_authenticated_key"] == "new789"
|
||||||
@ -2380,8 +2422,10 @@ async def test_import_addon_installed(
|
|||||||
set_addon_options,
|
set_addon_options,
|
||||||
start_addon,
|
start_addon,
|
||||||
get_addon_discovery_info,
|
get_addon_discovery_info,
|
||||||
|
serial_port,
|
||||||
):
|
):
|
||||||
"""Test import step while add-on already installed on Supervisor."""
|
"""Test import step while add-on already installed on Supervisor."""
|
||||||
|
serial_port.device = "/test/imported"
|
||||||
result = await hass.config_entries.flow.async_init(
|
result = await hass.config_entries.flow.async_init(
|
||||||
DOMAIN,
|
DOMAIN,
|
||||||
context={"source": config_entries.SOURCE_IMPORT},
|
context={"source": config_entries.SOURCE_IMPORT},
|
||||||
|
Loading…
x
Reference in New Issue
Block a user