diff --git a/homeassistant/components/homekit/__init__.py b/homeassistant/components/homekit/__init__.py index 19298a9f814..b293f1f542d 100644 --- a/homeassistant/components/homekit/__init__.py +++ b/homeassistant/components/homekit/__init__.py @@ -103,9 +103,9 @@ from .const import ( from .type_triggers import DeviceTriggerAccessory from .util import ( accessory_friendly_name, + async_port_is_available, dismiss_setup_message, get_persist_fullpath_for_entry_id, - port_is_available, remove_state_files_for_entry_id, show_setup_message, state_needs_accessory_mode, @@ -330,7 +330,7 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: logged_shutdown_wait = False for _ in range(0, SHUTDOWN_TIMEOUT): - if await hass.async_add_executor_job(port_is_available, entry.data[CONF_PORT]): + if async_port_is_available(entry.data[CONF_PORT]): break if not logged_shutdown_wait: diff --git a/homeassistant/components/homekit/config_flow.py b/homeassistant/components/homekit/config_flow.py index 6f0e9d9ba5f..81f439c8954 100644 --- a/homeassistant/components/homekit/config_flow.py +++ b/homeassistant/components/homekit/config_flow.py @@ -172,9 +172,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_pairing(self, user_input=None): """Pairing instructions.""" if user_input is not None: - port = await async_find_next_available_port( - self.hass, DEFAULT_CONFIG_FLOW_PORT - ) + port = async_find_next_available_port(self.hass, DEFAULT_CONFIG_FLOW_PORT) await self._async_add_entries_for_accessory_mode_entities(port) self.hk_data[CONF_PORT] = port include_domains_filter = self.hk_data[CONF_FILTER][CONF_INCLUDE_DOMAINS] @@ -205,7 +203,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): for entity_id in accessory_mode_entity_ids: if entity_id in exiting_entity_ids_accessory_mode: continue - port = await async_find_next_available_port(self.hass, next_port_to_check) + port = async_find_next_available_port(self.hass, next_port_to_check) next_port_to_check = port + 1 self.hass.async_create_task( self.hass.config_entries.flow.async_init( diff --git a/homeassistant/components/homekit/util.py b/homeassistant/components/homekit/util.py index 6585e9e9c4e..a5c9f3937ea 100644 --- a/homeassistant/components/homekit/util.py +++ b/homeassistant/components/homekit/util.py @@ -27,7 +27,7 @@ from homeassistant.const import ( CONF_TYPE, TEMP_CELSIUS, ) -from homeassistant.core import HomeAssistant, split_entity_id +from homeassistant.core import HomeAssistant, callback, split_entity_id import homeassistant.helpers.config_validation as cv from homeassistant.helpers.storage import STORAGE_DIR import homeassistant.util.temperature as temp_util @@ -433,34 +433,32 @@ def _get_test_socket(): return test_socket -def port_is_available(port: int) -> bool: +@callback +def async_port_is_available(port: int) -> bool: """Check to see if a port is available.""" - test_socket = _get_test_socket() try: - test_socket.bind(("", port)) + _get_test_socket().bind(("", port)) except OSError: return False - return True -async def async_find_next_available_port(hass: HomeAssistant, start_port: int) -> int: +@callback +def async_find_next_available_port(hass: HomeAssistant, start_port: int) -> int: """Find the next available port not assigned to a config entry.""" exclude_ports = { entry.data[CONF_PORT] for entry in hass.config_entries.async_entries(DOMAIN) if CONF_PORT in entry.data } - - return await hass.async_add_executor_job( - _find_next_available_port, start_port, exclude_ports - ) + return _async_find_next_available_port(start_port, exclude_ports) -def _find_next_available_port(start_port: int, exclude_ports: set) -> int: +@callback +def _async_find_next_available_port(start_port: int, exclude_ports: set) -> int: """Find the next available port starting with the given port.""" test_socket = _get_test_socket() - for port in range(start_port, MAX_PORT): + for port in range(start_port, MAX_PORT + 1): if port in exclude_ports: continue try: diff --git a/tests/components/homekit/test_config_flow.py b/tests/components/homekit/test_config_flow.py index b2e9af3816a..fadb4572df3 100644 --- a/tests/components/homekit/test_config_flow.py +++ b/tests/components/homekit/test_config_flow.py @@ -951,10 +951,15 @@ async def test_converting_bridge_to_accessory_mode(hass, hk_driver, mock_get_sou assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM assert result2["step_id"] == "cameras" - result3 = await hass.config_entries.options.async_configure( - result2["flow_id"], - user_input={"camera_copy": ["camera.tv"]}, - ) + with patch( + "homeassistant.components.homekit.async_setup_entry", + return_value=True, + ) as mock_setup_entry: + result3 = await hass.config_entries.options.async_configure( + result2["flow_id"], + user_input={"camera_copy": ["camera.tv"]}, + ) + await hass.async_block_till_done() assert result3["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY assert config_entry.options == { @@ -968,6 +973,7 @@ async def test_converting_bridge_to_accessory_mode(hass, hk_driver, mock_get_sou "include_entities": ["camera.tv"], }, } + assert len(mock_setup_entry.mock_calls) == 1 def _get_schema_default(schema, key_name): diff --git a/tests/components/homekit/test_homekit.py b/tests/components/homekit/test_homekit.py index b1ea2ab2a1d..e9c9ad6662b 100644 --- a/tests/components/homekit/test_homekit.py +++ b/tests/components/homekit/test_homekit.py @@ -1308,7 +1308,7 @@ async def test_homekit_uses_system_zeroconf(hass, hk_driver, mock_zeroconf): with patch("pyhap.accessory_driver.AccessoryDriver.async_start"), patch( f"{PATH_HOMEKIT}.HomeKit.async_stop" - ), patch(f"{PATH_HOMEKIT}.port_is_available"): + ), patch(f"{PATH_HOMEKIT}.async_port_is_available"): entry.add_to_hass(hass) assert await hass.config_entries.async_setup(entry.entry_id) await hass.async_block_till_done() @@ -1701,7 +1701,7 @@ async def test_wait_for_port_to_free(hass, hk_driver, mock_zeroconf, caplog): with patch("pyhap.accessory_driver.AccessoryDriver.async_start"), patch( f"{PATH_HOMEKIT}.HomeKit.async_stop" - ), patch(f"{PATH_HOMEKIT}.port_is_available", return_value=True) as port_mock: + ), patch(f"{PATH_HOMEKIT}.async_port_is_available", return_value=True) as port_mock: assert await hass.config_entries.async_setup(entry.entry_id) await hass.async_block_till_done() assert await hass.config_entries.async_unload(entry.entry_id) @@ -1712,7 +1712,7 @@ async def test_wait_for_port_to_free(hass, hk_driver, mock_zeroconf, caplog): with patch("pyhap.accessory_driver.AccessoryDriver.async_start"), patch( f"{PATH_HOMEKIT}.HomeKit.async_stop" ), patch.object(homekit_base, "PORT_CLEANUP_CHECK_INTERVAL_SECS", 0), patch( - f"{PATH_HOMEKIT}.port_is_available", return_value=False + f"{PATH_HOMEKIT}.async_port_is_available", return_value=False ) as port_mock: assert await hass.config_entries.async_setup(entry.entry_id) await hass.async_block_till_done() diff --git a/tests/components/homekit/test_util.py b/tests/components/homekit/test_util.py index 33c5c8623d1..2d4ac2171da 100644 --- a/tests/components/homekit/test_util.py +++ b/tests/components/homekit/test_util.py @@ -1,5 +1,5 @@ """Test HomeKit util module.""" -from unittest.mock import Mock +from unittest.mock import MagicMock, Mock, patch import pytest import voluptuous as vol @@ -26,12 +26,12 @@ from homeassistant.components.homekit.const import ( from homeassistant.components.homekit.util import ( accessory_friendly_name, async_find_next_available_port, + async_port_is_available, cleanup_name_for_homekit, convert_to_float, density_to_air_quality, dismiss_setup_message, format_sw_version, - port_is_available, show_setup_message, state_needs_accessory_mode, temperature_to_homekit, @@ -61,6 +61,25 @@ from .util import async_init_integration from tests.common import MockConfigEntry, async_mock_service +def _mock_socket(failure_attempts: int = 0) -> MagicMock: + """Mock a socket that fails to bind failure_attempts amount of times.""" + mock_socket = MagicMock() + attempts = 0 + + def _simulate_bind(*_): + import pprint + + pprint.pprint("Calling bind") + nonlocal attempts + attempts += 1 + if attempts <= failure_attempts: + raise OSError + return + + mock_socket.bind = Mock(side_effect=_simulate_bind) + return mock_socket + + def test_validate_entity_config(): """Test validate entities.""" configs = [ @@ -257,11 +276,35 @@ async def test_dismiss_setup_msg(hass): async def test_port_is_available(hass): """Test we can get an available port and it is actually available.""" - next_port = await async_find_next_available_port(hass, DEFAULT_CONFIG_FLOW_PORT) - + with patch( + "homeassistant.components.homekit.util.socket.socket", + return_value=_mock_socket(0), + ): + next_port = async_find_next_available_port(hass, DEFAULT_CONFIG_FLOW_PORT) assert next_port + with patch( + "homeassistant.components.homekit.util.socket.socket", + return_value=_mock_socket(0), + ): + assert async_port_is_available(next_port) - assert await hass.async_add_executor_job(port_is_available, next_port) + with patch( + "homeassistant.components.homekit.util.socket.socket", + return_value=_mock_socket(5), + ): + next_port = async_find_next_available_port(hass, DEFAULT_CONFIG_FLOW_PORT) + assert next_port == DEFAULT_CONFIG_FLOW_PORT + 5 + with patch( + "homeassistant.components.homekit.util.socket.socket", + return_value=_mock_socket(0), + ): + assert async_port_is_available(next_port) + + with patch( + "homeassistant.components.homekit.util.socket.socket", + return_value=_mock_socket(1), + ): + assert not async_port_is_available(next_port) async def test_port_is_available_skips_existing_entries(hass): @@ -273,12 +316,38 @@ async def test_port_is_available_skips_existing_entries(hass): ) entry.add_to_hass(hass) - next_port = await async_find_next_available_port(hass, DEFAULT_CONFIG_FLOW_PORT) + with patch( + "homeassistant.components.homekit.util.socket.socket", + return_value=_mock_socket(), + ): + next_port = async_find_next_available_port(hass, DEFAULT_CONFIG_FLOW_PORT) - assert next_port - assert next_port != DEFAULT_CONFIG_FLOW_PORT + assert next_port == DEFAULT_CONFIG_FLOW_PORT + 1 - assert await hass.async_add_executor_job(port_is_available, next_port) + with patch( + "homeassistant.components.homekit.util.socket.socket", + return_value=_mock_socket(), + ): + assert async_port_is_available(next_port) + + with patch( + "homeassistant.components.homekit.util.socket.socket", + return_value=_mock_socket(4), + ): + next_port = async_find_next_available_port(hass, DEFAULT_CONFIG_FLOW_PORT) + + assert next_port == DEFAULT_CONFIG_FLOW_PORT + 5 + with patch( + "homeassistant.components.homekit.util.socket.socket", + return_value=_mock_socket(), + ): + assert async_port_is_available(next_port) + + with pytest.raises(OSError), patch( + "homeassistant.components.homekit.util.socket.socket", + return_value=_mock_socket(10), + ): + async_find_next_available_port(hass, 65530) async def test_format_sw_version():