mirror of
https://github.com/home-assistant/core.git
synced 2025-07-09 14:27:07 +00:00
Split timeout in lutron_caseta to increase configure timeout (#138875)
This commit is contained in:
parent
2f1ff5ab95
commit
06019e7995
@ -3,7 +3,6 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import asyncio
|
import asyncio
|
||||||
import contextlib
|
|
||||||
from itertools import chain
|
from itertools import chain
|
||||||
import logging
|
import logging
|
||||||
import ssl
|
import ssl
|
||||||
@ -37,11 +36,12 @@ from .const import (
|
|||||||
ATTR_SERIAL,
|
ATTR_SERIAL,
|
||||||
ATTR_TYPE,
|
ATTR_TYPE,
|
||||||
BRIDGE_DEVICE_ID,
|
BRIDGE_DEVICE_ID,
|
||||||
BRIDGE_TIMEOUT,
|
|
||||||
CONF_CA_CERTS,
|
CONF_CA_CERTS,
|
||||||
CONF_CERTFILE,
|
CONF_CERTFILE,
|
||||||
CONF_KEYFILE,
|
CONF_KEYFILE,
|
||||||
CONF_SUBTYPE,
|
CONF_SUBTYPE,
|
||||||
|
CONFIGURE_TIMEOUT,
|
||||||
|
CONNECT_TIMEOUT,
|
||||||
DOMAIN,
|
DOMAIN,
|
||||||
LUTRON_CASETA_BUTTON_EVENT,
|
LUTRON_CASETA_BUTTON_EVENT,
|
||||||
MANUFACTURER,
|
MANUFACTURER,
|
||||||
@ -161,28 +161,40 @@ async def async_setup_entry(
|
|||||||
keyfile = hass.config.path(entry.data[CONF_KEYFILE])
|
keyfile = hass.config.path(entry.data[CONF_KEYFILE])
|
||||||
certfile = hass.config.path(entry.data[CONF_CERTFILE])
|
certfile = hass.config.path(entry.data[CONF_CERTFILE])
|
||||||
ca_certs = hass.config.path(entry.data[CONF_CA_CERTS])
|
ca_certs = hass.config.path(entry.data[CONF_CA_CERTS])
|
||||||
bridge = None
|
connected_future: asyncio.Future[None] = hass.loop.create_future()
|
||||||
|
|
||||||
|
def _on_connect() -> None:
|
||||||
|
nonlocal connected_future
|
||||||
|
if not connected_future.done():
|
||||||
|
connected_future.set_result(None)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
bridge = Smartbridge.create_tls(
|
bridge = Smartbridge.create_tls(
|
||||||
hostname=host, keyfile=keyfile, certfile=certfile, ca_certs=ca_certs
|
hostname=host,
|
||||||
|
keyfile=keyfile,
|
||||||
|
certfile=certfile,
|
||||||
|
ca_certs=ca_certs,
|
||||||
|
on_connect_callback=_on_connect,
|
||||||
)
|
)
|
||||||
except ssl.SSLError:
|
except ssl.SSLError:
|
||||||
_LOGGER.error("Invalid certificate used to connect to bridge at %s", host)
|
_LOGGER.error("Invalid certificate used to connect to bridge at %s", host)
|
||||||
return False
|
return False
|
||||||
|
|
||||||
timed_out = True
|
connect_task = hass.async_create_task(bridge.connect())
|
||||||
with contextlib.suppress(TimeoutError):
|
for future, name, timeout in (
|
||||||
async with asyncio.timeout(BRIDGE_TIMEOUT):
|
(connected_future, "connect", CONNECT_TIMEOUT),
|
||||||
await bridge.connect()
|
(connect_task, "configure", CONFIGURE_TIMEOUT),
|
||||||
timed_out = False
|
):
|
||||||
|
try:
|
||||||
if timed_out or not bridge.is_connected():
|
async with asyncio.timeout(timeout):
|
||||||
|
await future
|
||||||
|
except TimeoutError as ex:
|
||||||
|
connect_task.cancel()
|
||||||
await bridge.close()
|
await bridge.close()
|
||||||
if timed_out:
|
raise ConfigEntryNotReady(f"Timed out on {name} for {host}") from ex
|
||||||
raise ConfigEntryNotReady(f"Timed out while trying to connect to {host}")
|
|
||||||
if not bridge.is_connected():
|
if not bridge.is_connected():
|
||||||
raise ConfigEntryNotReady(f"Cannot connect to {host}")
|
raise ConfigEntryNotReady(f"Connection failed to {host}")
|
||||||
|
|
||||||
_LOGGER.debug("Connected to Lutron Caseta bridge via LEAP at %s", host)
|
_LOGGER.debug("Connected to Lutron Caseta bridge via LEAP at %s", host)
|
||||||
await _async_migrate_unique_ids(hass, entry)
|
await _async_migrate_unique_ids(hass, entry)
|
||||||
|
@ -20,10 +20,11 @@ from homeassistant.helpers.service_info.zeroconf import ZeroconfServiceInfo
|
|||||||
from .const import (
|
from .const import (
|
||||||
ABORT_REASON_CANNOT_CONNECT,
|
ABORT_REASON_CANNOT_CONNECT,
|
||||||
BRIDGE_DEVICE_ID,
|
BRIDGE_DEVICE_ID,
|
||||||
BRIDGE_TIMEOUT,
|
|
||||||
CONF_CA_CERTS,
|
CONF_CA_CERTS,
|
||||||
CONF_CERTFILE,
|
CONF_CERTFILE,
|
||||||
CONF_KEYFILE,
|
CONF_KEYFILE,
|
||||||
|
CONFIGURE_TIMEOUT,
|
||||||
|
CONNECT_TIMEOUT,
|
||||||
DOMAIN,
|
DOMAIN,
|
||||||
ERROR_CANNOT_CONNECT,
|
ERROR_CANNOT_CONNECT,
|
||||||
STEP_IMPORT_FAILED,
|
STEP_IMPORT_FAILED,
|
||||||
@ -232,7 +233,7 @@ class LutronCasetaFlowHandler(ConfigFlow, domain=DOMAIN):
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
try:
|
try:
|
||||||
async with asyncio.timeout(BRIDGE_TIMEOUT):
|
async with asyncio.timeout(CONNECT_TIMEOUT + CONFIGURE_TIMEOUT):
|
||||||
await bridge.connect()
|
await bridge.connect()
|
||||||
except TimeoutError:
|
except TimeoutError:
|
||||||
_LOGGER.error(
|
_LOGGER.error(
|
||||||
|
@ -34,7 +34,8 @@ ACTION_RELEASE = "release"
|
|||||||
|
|
||||||
CONF_SUBTYPE = "subtype"
|
CONF_SUBTYPE = "subtype"
|
||||||
|
|
||||||
BRIDGE_TIMEOUT = 35
|
CONNECT_TIMEOUT = 9
|
||||||
|
CONFIGURE_TIMEOUT = 50
|
||||||
|
|
||||||
UNASSIGNED_AREA = "Unassigned"
|
UNASSIGNED_AREA = "Unassigned"
|
||||||
|
|
||||||
|
@ -1,5 +1,8 @@
|
|||||||
"""Tests for the Lutron Caseta integration."""
|
"""Tests for the Lutron Caseta integration."""
|
||||||
|
|
||||||
|
import asyncio
|
||||||
|
from collections.abc import Callable
|
||||||
|
from typing import Any
|
||||||
from unittest.mock import patch
|
from unittest.mock import patch
|
||||||
|
|
||||||
from homeassistant.components.lutron_caseta import DOMAIN
|
from homeassistant.components.lutron_caseta import DOMAIN
|
||||||
@ -84,25 +87,12 @@ _LEAP_DEVICE_TYPES = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
async def async_setup_integration(hass: HomeAssistant, mock_bridge) -> MockConfigEntry:
|
|
||||||
"""Set up a mock bridge."""
|
|
||||||
mock_entry = MockConfigEntry(domain=DOMAIN, data=ENTRY_MOCK_DATA)
|
|
||||||
mock_entry.add_to_hass(hass)
|
|
||||||
|
|
||||||
with patch(
|
|
||||||
"homeassistant.components.lutron_caseta.Smartbridge.create_tls"
|
|
||||||
) as create_tls:
|
|
||||||
create_tls.return_value = mock_bridge(can_connect=True)
|
|
||||||
await hass.config_entries.async_setup(mock_entry.entry_id)
|
|
||||||
await hass.async_block_till_done()
|
|
||||||
return mock_entry
|
|
||||||
|
|
||||||
|
|
||||||
class MockBridge:
|
class MockBridge:
|
||||||
"""Mock Lutron bridge that emulates configured connected status."""
|
"""Mock Lutron bridge that emulates configured connected status."""
|
||||||
|
|
||||||
def __init__(self, can_connect=True) -> None:
|
def __init__(self, can_connect=True, timeout_on_connect=False) -> None:
|
||||||
"""Initialize MockBridge instance with configured mock connectivity."""
|
"""Initialize MockBridge instance with configured mock connectivity."""
|
||||||
|
self.timeout_on_connect = timeout_on_connect
|
||||||
self.can_connect = can_connect
|
self.can_connect = can_connect
|
||||||
self.is_currently_connected = False
|
self.is_currently_connected = False
|
||||||
self.areas = self.load_areas()
|
self.areas = self.load_areas()
|
||||||
@ -113,6 +103,8 @@ class MockBridge:
|
|||||||
|
|
||||||
async def connect(self):
|
async def connect(self):
|
||||||
"""Connect the mock bridge."""
|
"""Connect the mock bridge."""
|
||||||
|
if self.timeout_on_connect:
|
||||||
|
await asyncio.Event().wait() # wait forever
|
||||||
if self.can_connect:
|
if self.can_connect:
|
||||||
self.is_currently_connected = True
|
self.is_currently_connected = True
|
||||||
|
|
||||||
@ -320,3 +312,43 @@ class MockBridge:
|
|||||||
async def close(self):
|
async def close(self):
|
||||||
"""Close the mock bridge connection."""
|
"""Close the mock bridge connection."""
|
||||||
self.is_currently_connected = False
|
self.is_currently_connected = False
|
||||||
|
|
||||||
|
|
||||||
|
def make_mock_entry() -> MockConfigEntry:
|
||||||
|
"""Create a mock config entry."""
|
||||||
|
return MockConfigEntry(domain=DOMAIN, data=ENTRY_MOCK_DATA)
|
||||||
|
|
||||||
|
|
||||||
|
async def async_setup_integration(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
mock_bridge: MockBridge,
|
||||||
|
config_entry_id: str | None = None,
|
||||||
|
can_connect: bool = True,
|
||||||
|
timeout_during_connect: bool = False,
|
||||||
|
timeout_during_configure: bool = False,
|
||||||
|
) -> MockConfigEntry:
|
||||||
|
"""Set up a mock bridge."""
|
||||||
|
if config_entry_id is None:
|
||||||
|
mock_entry = make_mock_entry()
|
||||||
|
mock_entry.add_to_hass(hass)
|
||||||
|
config_entry_id = mock_entry.entry_id
|
||||||
|
else:
|
||||||
|
mock_entry = hass.config_entries.async_get_entry(config_entry_id)
|
||||||
|
|
||||||
|
def create_tls_factory(
|
||||||
|
*args: Any, on_connect_callback: Callable[[], None], **kwargs: Any
|
||||||
|
) -> None:
|
||||||
|
"""Return a mock bridge."""
|
||||||
|
if not timeout_during_connect:
|
||||||
|
on_connect_callback()
|
||||||
|
return mock_bridge(
|
||||||
|
can_connect=can_connect, timeout_on_connect=timeout_during_configure
|
||||||
|
)
|
||||||
|
|
||||||
|
with patch(
|
||||||
|
"homeassistant.components.lutron_caseta.Smartbridge.create_tls",
|
||||||
|
create_tls_factory,
|
||||||
|
):
|
||||||
|
await hass.config_entries.async_setup(config_entry_id)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
return mock_entry
|
||||||
|
@ -1,7 +1,5 @@
|
|||||||
"""The tests for Lutron Caséta device triggers."""
|
"""The tests for Lutron Caséta device triggers."""
|
||||||
|
|
||||||
from unittest.mock import patch
|
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
from pytest_unordered import unordered
|
from pytest_unordered import unordered
|
||||||
|
|
||||||
@ -37,7 +35,7 @@ from homeassistant.core import HomeAssistant, ServiceCall
|
|||||||
from homeassistant.helpers import device_registry as dr
|
from homeassistant.helpers import device_registry as dr
|
||||||
from homeassistant.setup import async_setup_component
|
from homeassistant.setup import async_setup_component
|
||||||
|
|
||||||
from . import MockBridge
|
from . import MockBridge, async_setup_integration
|
||||||
|
|
||||||
from tests.common import MockConfigEntry, async_get_device_automations
|
from tests.common import MockConfigEntry, async_get_device_automations
|
||||||
|
|
||||||
@ -112,12 +110,7 @@ async def _async_setup_lutron_with_picos(hass: HomeAssistant) -> str:
|
|||||||
)
|
)
|
||||||
config_entry.add_to_hass(hass)
|
config_entry.add_to_hass(hass)
|
||||||
|
|
||||||
with patch(
|
await async_setup_integration(hass, MockBridge, config_entry.entry_id)
|
||||||
"homeassistant.components.lutron_caseta.Smartbridge.create_tls",
|
|
||||||
return_value=MockBridge(can_connect=True),
|
|
||||||
):
|
|
||||||
await hass.config_entries.async_setup(config_entry.entry_id)
|
|
||||||
await hass.async_block_till_done()
|
|
||||||
|
|
||||||
return config_entry.entry_id
|
return config_entry.entry_id
|
||||||
|
|
||||||
@ -487,9 +480,7 @@ async def test_if_fires_on_button_event_late_setup(
|
|||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
with patch("homeassistant.components.lutron_caseta.Smartbridge.create_tls"):
|
await async_setup_integration(hass, MockBridge, config_entry_id)
|
||||||
await hass.config_entries.async_setup(config_entry_id)
|
|
||||||
await hass.async_block_till_done()
|
|
||||||
|
|
||||||
message = {
|
message = {
|
||||||
ATTR_SERIAL: device.get("serial"),
|
ATTR_SERIAL: device.get("serial"),
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
"""Test the Lutron Caseta diagnostics."""
|
"""Test the Lutron Caseta diagnostics."""
|
||||||
|
|
||||||
from unittest.mock import ANY, patch
|
from unittest.mock import ANY
|
||||||
|
|
||||||
from homeassistant.components.lutron_caseta import DOMAIN
|
from homeassistant.components.lutron_caseta import DOMAIN
|
||||||
from homeassistant.components.lutron_caseta.const import (
|
from homeassistant.components.lutron_caseta.const import (
|
||||||
@ -11,7 +11,7 @@ from homeassistant.components.lutron_caseta.const import (
|
|||||||
from homeassistant.const import CONF_HOST
|
from homeassistant.const import CONF_HOST
|
||||||
from homeassistant.core import HomeAssistant
|
from homeassistant.core import HomeAssistant
|
||||||
|
|
||||||
from . import MockBridge
|
from . import MockBridge, async_setup_integration
|
||||||
|
|
||||||
from tests.common import MockConfigEntry
|
from tests.common import MockConfigEntry
|
||||||
from tests.components.diagnostics import get_diagnostics_for_config_entry
|
from tests.components.diagnostics import get_diagnostics_for_config_entry
|
||||||
@ -34,12 +34,7 @@ async def test_diagnostics(
|
|||||||
)
|
)
|
||||||
config_entry.add_to_hass(hass)
|
config_entry.add_to_hass(hass)
|
||||||
|
|
||||||
with patch(
|
await async_setup_integration(hass, MockBridge, config_entry.entry_id)
|
||||||
"homeassistant.components.lutron_caseta.Smartbridge.create_tls",
|
|
||||||
return_value=MockBridge(can_connect=True),
|
|
||||||
):
|
|
||||||
await hass.config_entries.async_setup(config_entry.entry_id)
|
|
||||||
await hass.async_block_till_done()
|
|
||||||
|
|
||||||
diag = await get_diagnostics_for_config_entry(hass, hass_client, config_entry)
|
diag = await get_diagnostics_for_config_entry(hass, hass_client, config_entry)
|
||||||
assert diag == {
|
assert diag == {
|
||||||
|
54
tests/components/lutron_caseta/test_init.py
Normal file
54
tests/components/lutron_caseta/test_init.py
Normal file
@ -0,0 +1,54 @@
|
|||||||
|
"""Tests for the Lutron Caseta integration."""
|
||||||
|
|
||||||
|
from unittest.mock import patch
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from homeassistant.components import lutron_caseta
|
||||||
|
from homeassistant.config_entries import ConfigEntryState
|
||||||
|
from homeassistant.core import HomeAssistant
|
||||||
|
|
||||||
|
from . import MockBridge, async_setup_integration, make_mock_entry
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
("constant", "message", "timeout_during_connect", "timeout_during_configure"),
|
||||||
|
[
|
||||||
|
("CONNECT_TIMEOUT", "Timed out on connect", True, False),
|
||||||
|
("CONFIGURE_TIMEOUT", "Timed out on configure", False, True),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
async def test_timeout_during_setup(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
caplog: pytest.LogCaptureFixture,
|
||||||
|
constant: str,
|
||||||
|
message: str,
|
||||||
|
timeout_during_connect: bool,
|
||||||
|
timeout_during_configure: bool,
|
||||||
|
) -> None:
|
||||||
|
"""Test a timeout during setup."""
|
||||||
|
mock_entry = make_mock_entry()
|
||||||
|
mock_entry.add_to_hass(hass)
|
||||||
|
with patch.object(lutron_caseta, constant, 0.001):
|
||||||
|
await async_setup_integration(
|
||||||
|
hass,
|
||||||
|
MockBridge,
|
||||||
|
config_entry_id=mock_entry.entry_id,
|
||||||
|
timeout_during_connect=timeout_during_connect,
|
||||||
|
timeout_during_configure=timeout_during_configure,
|
||||||
|
)
|
||||||
|
assert mock_entry.state is ConfigEntryState.SETUP_RETRY
|
||||||
|
assert f"{message} for 1.1.1.1" in caplog.text
|
||||||
|
|
||||||
|
|
||||||
|
async def test_cannot_connect(
|
||||||
|
hass: HomeAssistant, caplog: pytest.LogCaptureFixture
|
||||||
|
) -> None:
|
||||||
|
"""Test failing to connect."""
|
||||||
|
mock_entry = make_mock_entry()
|
||||||
|
mock_entry.add_to_hass(hass)
|
||||||
|
await async_setup_integration(
|
||||||
|
hass, MockBridge, config_entry_id=mock_entry.entry_id, can_connect=False
|
||||||
|
)
|
||||||
|
assert mock_entry.state is ConfigEntryState.SETUP_RETRY
|
||||||
|
assert "Connection failed to 1.1.1.1" in caplog.text
|
@ -1,7 +1,5 @@
|
|||||||
"""The tests for lutron caseta logbook."""
|
"""The tests for lutron caseta logbook."""
|
||||||
|
|
||||||
from unittest.mock import patch
|
|
||||||
|
|
||||||
from homeassistant.components.lutron_caseta.const import (
|
from homeassistant.components.lutron_caseta.const import (
|
||||||
ATTR_ACTION,
|
ATTR_ACTION,
|
||||||
ATTR_AREA_NAME,
|
ATTR_AREA_NAME,
|
||||||
@ -43,13 +41,7 @@ async def test_humanify_lutron_caseta_button_event(hass: HomeAssistant) -> None:
|
|||||||
unique_id="abc",
|
unique_id="abc",
|
||||||
)
|
)
|
||||||
config_entry.add_to_hass(hass)
|
config_entry.add_to_hass(hass)
|
||||||
|
await async_setup_integration(hass, MockBridge, config_entry.entry_id)
|
||||||
with patch(
|
|
||||||
"homeassistant.components.lutron_caseta.Smartbridge.create_tls",
|
|
||||||
return_value=MockBridge(can_connect=True),
|
|
||||||
):
|
|
||||||
await hass.config_entries.async_setup(config_entry.entry_id)
|
|
||||||
await hass.async_block_till_done()
|
|
||||||
|
|
||||||
await hass.async_block_till_done()
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
@ -104,12 +96,7 @@ async def test_humanify_lutron_caseta_button_event_integration_not_loaded(
|
|||||||
)
|
)
|
||||||
config_entry.add_to_hass(hass)
|
config_entry.add_to_hass(hass)
|
||||||
|
|
||||||
with patch(
|
await async_setup_integration(hass, MockBridge, config_entry.entry_id)
|
||||||
"homeassistant.components.lutron_caseta.Smartbridge.create_tls",
|
|
||||||
return_value=MockBridge(can_connect=True),
|
|
||||||
):
|
|
||||||
await hass.config_entries.async_setup(config_entry.entry_id)
|
|
||||||
await hass.async_block_till_done()
|
|
||||||
|
|
||||||
await hass.config_entries.async_unload(config_entry.entry_id)
|
await hass.config_entries.async_unload(config_entry.entry_id)
|
||||||
await hass.async_block_till_done()
|
await hass.async_block_till_done()
|
||||||
|
Loading…
x
Reference in New Issue
Block a user