HA config_flow: transport dropdown (TCP/UDP) for the new UDP path
custom_components/omni_pca/const.py:
+ CONF_TRANSPORT, TRANSPORT_TCP, TRANSPORT_UDP, DEFAULT_TRANSPORT='tcp'
custom_components/omni_pca/config_flow.py:
+ 'transport' field in _USER_SCHEMA with vol.In([tcp, udp]),
default tcp (so existing flows are unchanged)
+ transport stored in entry.data on create
+ reauth carries the existing transport over from entry.data
+ _probe() takes transport=, propagates to OmniClient
custom_components/omni_pca/coordinator.py:
+ transport= constructor arg, defaults to 'tcp'
+ _ensure_connected passes transport= through to OmniClient
custom_components/omni_pca/__init__.py:
+ reads transport from entry.data (default tcp), passes to coordinator
Backward-compat: existing config entries without a transport key fall
through to 'tcp', identical to current behavior. New entries get the
choice at the config-flow form. The reauth step preserves the existing
transport so users don't have to re-pick it.
357 tests pass; ruff clean across src/ tests/ custom_components/.
HA integration tests don't need updating because they don't pass
transport= explicitly (default tcp matches the mock's default).
This commit is contained in:
parent
7f82dbbbfa
commit
81725b4dbf
@ -14,7 +14,13 @@ from typing import TYPE_CHECKING
|
|||||||
from homeassistant.const import CONF_HOST, CONF_PORT, Platform
|
from homeassistant.const import CONF_HOST, CONF_PORT, Platform
|
||||||
from homeassistant.exceptions import ConfigEntryNotReady
|
from homeassistant.exceptions import ConfigEntryNotReady
|
||||||
|
|
||||||
from .const import CONF_CONTROLLER_KEY, DOMAIN, LOGGER
|
from .const import (
|
||||||
|
CONF_CONTROLLER_KEY,
|
||||||
|
CONF_TRANSPORT,
|
||||||
|
DEFAULT_TRANSPORT,
|
||||||
|
DOMAIN,
|
||||||
|
LOGGER,
|
||||||
|
)
|
||||||
from .coordinator import OmniDataUpdateCoordinator
|
from .coordinator import OmniDataUpdateCoordinator
|
||||||
from .services import async_setup_services, async_unload_services
|
from .services import async_setup_services, async_unload_services
|
||||||
|
|
||||||
@ -50,12 +56,14 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
|||||||
LOGGER.error("stored controller key for %s is corrupt: %s", entry.title, err)
|
LOGGER.error("stored controller key for %s is corrupt: %s", entry.title, err)
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
transport: str = entry.data.get(CONF_TRANSPORT, DEFAULT_TRANSPORT)
|
||||||
coordinator = OmniDataUpdateCoordinator(
|
coordinator = OmniDataUpdateCoordinator(
|
||||||
hass,
|
hass,
|
||||||
entry,
|
entry,
|
||||||
host=host,
|
host=host,
|
||||||
port=port,
|
port=port,
|
||||||
controller_key=controller_key,
|
controller_key=controller_key,
|
||||||
|
transport=transport,
|
||||||
)
|
)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
|||||||
@ -20,10 +20,14 @@ from omni_pca.connection import (
|
|||||||
|
|
||||||
from .const import (
|
from .const import (
|
||||||
CONF_CONTROLLER_KEY,
|
CONF_CONTROLLER_KEY,
|
||||||
|
CONF_TRANSPORT,
|
||||||
CONTROLLER_KEY_HEX_LEN,
|
CONTROLLER_KEY_HEX_LEN,
|
||||||
DEFAULT_PORT,
|
DEFAULT_PORT,
|
||||||
|
DEFAULT_TRANSPORT,
|
||||||
DOMAIN,
|
DOMAIN,
|
||||||
LOGGER,
|
LOGGER,
|
||||||
|
TRANSPORT_TCP,
|
||||||
|
TRANSPORT_UDP,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@ -60,6 +64,12 @@ _USER_SCHEMA = vol.Schema(
|
|||||||
vol.Coerce(int), vol.Range(min=1, max=65535)
|
vol.Coerce(int), vol.Range(min=1, max=65535)
|
||||||
),
|
),
|
||||||
vol.Required(CONF_CONTROLLER_KEY): str,
|
vol.Required(CONF_CONTROLLER_KEY): str,
|
||||||
|
# Most modern firmware uses TCP; some installers configure
|
||||||
|
# Network_UDP. PC Access stores the choice as
|
||||||
|
# enuPreferredNetworkProtocol in the .pca config.
|
||||||
|
vol.Required(CONF_TRANSPORT, default=DEFAULT_TRANSPORT): vol.In(
|
||||||
|
[TRANSPORT_TCP, TRANSPORT_UDP]
|
||||||
|
),
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -79,6 +89,7 @@ class OmniConfigFlow(ConfigFlow, domain=DOMAIN):
|
|||||||
if user_input is not None:
|
if user_input is not None:
|
||||||
host: str = user_input[CONF_HOST].strip()
|
host: str = user_input[CONF_HOST].strip()
|
||||||
port: int = user_input[CONF_PORT]
|
port: int = user_input[CONF_PORT]
|
||||||
|
transport: str = user_input.get(CONF_TRANSPORT, DEFAULT_TRANSPORT)
|
||||||
unique_id = f"{host}:{port}"
|
unique_id = f"{host}:{port}"
|
||||||
|
|
||||||
await self.async_set_unique_id(unique_id)
|
await self.async_set_unique_id(unique_id)
|
||||||
@ -90,7 +101,7 @@ class OmniConfigFlow(ConfigFlow, domain=DOMAIN):
|
|||||||
LOGGER.debug("controller key rejected: %s", err)
|
LOGGER.debug("controller key rejected: %s", err)
|
||||||
errors[CONF_CONTROLLER_KEY] = "invalid_key"
|
errors[CONF_CONTROLLER_KEY] = "invalid_key"
|
||||||
else:
|
else:
|
||||||
title, error = await self._probe(host, port, key)
|
title, error = await self._probe(host, port, key, transport)
|
||||||
if error is not None:
|
if error is not None:
|
||||||
errors["base"] = error
|
errors["base"] = error
|
||||||
else:
|
else:
|
||||||
@ -100,6 +111,7 @@ class OmniConfigFlow(ConfigFlow, domain=DOMAIN):
|
|||||||
CONF_HOST: host,
|
CONF_HOST: host,
|
||||||
CONF_PORT: port,
|
CONF_PORT: port,
|
||||||
CONF_CONTROLLER_KEY: key.hex(),
|
CONF_CONTROLLER_KEY: key.hex(),
|
||||||
|
CONF_TRANSPORT: transport,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -121,6 +133,9 @@ class OmniConfigFlow(ConfigFlow, domain=DOMAIN):
|
|||||||
assert self._reauth_entry_data is not None
|
assert self._reauth_entry_data is not None
|
||||||
host: str = self._reauth_entry_data[CONF_HOST]
|
host: str = self._reauth_entry_data[CONF_HOST]
|
||||||
port: int = self._reauth_entry_data[CONF_PORT]
|
port: int = self._reauth_entry_data[CONF_PORT]
|
||||||
|
transport: str = self._reauth_entry_data.get(
|
||||||
|
CONF_TRANSPORT, DEFAULT_TRANSPORT
|
||||||
|
)
|
||||||
errors: dict[str, str] = {}
|
errors: dict[str, str] = {}
|
||||||
|
|
||||||
if user_input is not None:
|
if user_input is not None:
|
||||||
@ -129,7 +144,7 @@ class OmniConfigFlow(ConfigFlow, domain=DOMAIN):
|
|||||||
except InvalidControllerKey:
|
except InvalidControllerKey:
|
||||||
errors[CONF_CONTROLLER_KEY] = "invalid_key"
|
errors[CONF_CONTROLLER_KEY] = "invalid_key"
|
||||||
else:
|
else:
|
||||||
_, error = await self._probe(host, port, key)
|
_, error = await self._probe(host, port, key, transport)
|
||||||
if error is not None:
|
if error is not None:
|
||||||
errors["base"] = error
|
errors["base"] = error
|
||||||
else:
|
else:
|
||||||
@ -147,11 +162,20 @@ class OmniConfigFlow(ConfigFlow, domain=DOMAIN):
|
|||||||
# ---- helpers ---------------------------------------------------------
|
# ---- helpers ---------------------------------------------------------
|
||||||
|
|
||||||
async def _probe(
|
async def _probe(
|
||||||
self, host: str, port: int, key: bytes
|
self,
|
||||||
|
host: str,
|
||||||
|
port: int,
|
||||||
|
key: bytes,
|
||||||
|
transport: str = DEFAULT_TRANSPORT,
|
||||||
) -> tuple[str | None, str | None]:
|
) -> tuple[str | None, str | None]:
|
||||||
"""Try to connect once. Returns (title, error_code)."""
|
"""Try to connect once. Returns (title, error_code)."""
|
||||||
try:
|
try:
|
||||||
async with OmniClient(host, port=port, controller_key=key) as client:
|
async with OmniClient(
|
||||||
|
host,
|
||||||
|
port=port,
|
||||||
|
controller_key=key,
|
||||||
|
transport=transport, # type: ignore[arg-type]
|
||||||
|
) as client:
|
||||||
info = await client.get_system_information()
|
info = await client.get_system_information()
|
||||||
except (HandshakeError, InvalidEncryptionKeyError):
|
except (HandshakeError, InvalidEncryptionKeyError):
|
||||||
return None, "invalid_auth"
|
return None, "invalid_auth"
|
||||||
|
|||||||
@ -12,6 +12,11 @@ DEFAULT_PORT: Final = 4369
|
|||||||
DEFAULT_TIMEOUT: Final = 5.0
|
DEFAULT_TIMEOUT: Final = 5.0
|
||||||
|
|
||||||
CONF_CONTROLLER_KEY: Final = "controller_key"
|
CONF_CONTROLLER_KEY: Final = "controller_key"
|
||||||
|
CONF_TRANSPORT: Final = "transport"
|
||||||
|
|
||||||
|
TRANSPORT_TCP: Final = "tcp"
|
||||||
|
TRANSPORT_UDP: Final = "udp"
|
||||||
|
DEFAULT_TRANSPORT: Final = TRANSPORT_TCP
|
||||||
|
|
||||||
MANUFACTURER: Final = "HAI / Leviton"
|
MANUFACTURER: Final = "HAI / Leviton"
|
||||||
|
|
||||||
|
|||||||
@ -137,6 +137,7 @@ class OmniDataUpdateCoordinator(DataUpdateCoordinator[OmniData]):
|
|||||||
host: str,
|
host: str,
|
||||||
port: int,
|
port: int,
|
||||||
controller_key: bytes,
|
controller_key: bytes,
|
||||||
|
transport: str = "tcp",
|
||||||
) -> None:
|
) -> None:
|
||||||
super().__init__(
|
super().__init__(
|
||||||
hass,
|
hass,
|
||||||
@ -148,6 +149,7 @@ class OmniDataUpdateCoordinator(DataUpdateCoordinator[OmniData]):
|
|||||||
self._host = host
|
self._host = host
|
||||||
self._port = port
|
self._port = port
|
||||||
self._controller_key = controller_key
|
self._controller_key = controller_key
|
||||||
|
self._transport = transport
|
||||||
self._client: OmniClient | None = None
|
self._client: OmniClient | None = None
|
||||||
self._discovery_done = False
|
self._discovery_done = False
|
||||||
self._discovered: OmniData | None = None
|
self._discovered: OmniData | None = None
|
||||||
@ -236,6 +238,7 @@ class OmniDataUpdateCoordinator(DataUpdateCoordinator[OmniData]):
|
|||||||
self._host,
|
self._host,
|
||||||
port=self._port,
|
port=self._port,
|
||||||
controller_key=self._controller_key,
|
controller_key=self._controller_key,
|
||||||
|
transport=self._transport, # type: ignore[arg-type]
|
||||||
)
|
)
|
||||||
# Drive __aenter__ manually so the client survives across update
|
# Drive __aenter__ manually so the client survives across update
|
||||||
# cycles; we close it explicitly on shutdown / failure.
|
# cycles; we close it explicitly on shutdown / failure.
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user