diff --git a/homeassistant/components/tplink_omada/__init__.py b/homeassistant/components/tplink_omada/__init__.py index 7ea7fd95fef..af1f882d38c 100644 --- a/homeassistant/components/tplink_omada/__init__.py +++ b/homeassistant/components/tplink_omada/__init__.py @@ -14,7 +14,12 @@ from tplink_omada_client.exceptions import ( from homeassistant.config_entries import ConfigEntry from homeassistant.const import Platform from homeassistant.core import HomeAssistant, ServiceCall -from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady +from homeassistant.exceptions import ( + ConfigEntryAuthFailed, + ConfigEntryNotReady, + HomeAssistantError, + ServiceValidationError, +) from homeassistant.helpers import device_registry as dr from .config_flow import CONF_SITE, create_omada_client @@ -61,12 +66,15 @@ async def async_setup_entry(hass: HomeAssistant, entry: OmadaConfigEntry) -> boo entry.runtime_data = controller async def handle_reconnect_client(call: ServiceCall) -> None: - """Handle the service action call.""" + """Handle the service action to force reconnection of a network client.""" mac: str | None = call.data.get("mac") if not mac: - return + raise ServiceValidationError("MAC address is required") - await site_client.reconnect_client(mac) + try: + await site_client.reconnect_client(mac) + except OmadaClientException as ex: + raise HomeAssistantError("Failed to reconnect client") from ex hass.services.async_register(DOMAIN, "reconnect_client", handle_reconnect_client) diff --git a/homeassistant/components/tplink_omada/quality_scale.yaml b/homeassistant/components/tplink_omada/quality_scale.yaml new file mode 100644 index 00000000000..7bb37b19cf4 --- /dev/null +++ b/homeassistant/components/tplink_omada/quality_scale.yaml @@ -0,0 +1,80 @@ +rules: + # Bronze + action-setup: + status: todo + comment: Actions are created in async_setup_entry, and need to be moved. + appropriate-polling: + status: done + comment: Service data APIs are polled every 5 minutes + brands: done + common-modules: + status: todo + comment: The coordinator for the update platform should be moved to common module. + config-flow-test-coverage: + status: todo + comment: "test_form_single_site is patching config flow internals, and should only patch external APIs. Must address feedback from #156697." + config-flow: done + dependency-transparency: + status: done + comment: API dependency published on PyPI, MIT licensed. + docs-actions: done + docs-high-level-description: done + docs-installation-instructions: done + docs-removal-instructions: done + entity-event-setup: + status: exempt + comment: Integration does not subscribe to events. + entity-unique-id: done + has-entity-name: done + runtime-data: done + test-before-configure: done + test-before-setup: done + unique-config-entry: + status: done + comment: Omada Site unique ID checked during config flow. + + # Silver + action-exceptions: done + config-entry-unloading: done + docs-configuration-parameters: + status: exempt + comment: No configuration parameters or options flow yet. + docs-installation-parameters: done + entity-unavailable: done + integration-owner: done + log-when-unavailable: done + parallel-updates: todo + reauthentication-flow: done + test-coverage: todo + + # Gold + devices: done + diagnostics: todo + discovery-update-info: todo + discovery: todo + docs-data-update: todo + docs-examples: todo + docs-known-limitations: todo + docs-supported-devices: done + docs-supported-functions: todo + docs-troubleshooting: todo + docs-use-cases: todo + dynamic-devices: todo + entity-category: done + entity-device-class: done + entity-disabled-by-default: todo + entity-translations: done + exception-translations: todo + icon-translations: done + reconfiguration-flow: todo + repair-issues: todo + stale-devices: + status: todo + comment: Stale devices are auto-deleted at startup, not yet during runtime. + + # Platinum + async-dependency: done + inject-websession: + status: done + comment: Uses async_create_clientsession in case where unsafe cookies are needed. + strict-typing: todo diff --git a/homeassistant/components/tplink_omada/strings.json b/homeassistant/components/tplink_omada/strings.json index c430193db66..6847d165c9a 100644 --- a/homeassistant/components/tplink_omada/strings.json +++ b/homeassistant/components/tplink_omada/strings.json @@ -17,6 +17,10 @@ "password": "[%key:common::config_flow::data::password%]", "username": "[%key:common::config_flow::data::username%]" }, + "data_description": { + "password": "Password for the Omada controller user.", + "username": "Username for the Omada controller user." + }, "description": "The provided credentials have stopped working. Please update them.", "title": "Update TP-Link Omada credentials" }, @@ -24,7 +28,10 @@ "data": { "site": "Site" }, - "title": "Choose which site(s) to manage" + "data_description": { + "site": "Select the site you want to manage in Home Assistant." + }, + "title": "Choose which site to manage" }, "user": { "data": { @@ -34,7 +41,10 @@ "verify_ssl": "[%key:common::config_flow::data::verify_ssl%]" }, "data_description": { - "host": "URL of the management interface of your TP-Link Omada controller." + "host": "URL of the management interface of your TP-Link Omada controller.", + "password": "Password for the Omada controller user.", + "username": "Username for the Omada controller user.", + "verify_ssl": "Uncheck this box if you are using the default self-signed certificate on the controller." }, "description": "Enter the connection details for the Omada controller. Cloud controllers aren't supported." } diff --git a/script/hassfest/quality_scale.py b/script/hassfest/quality_scale.py index ebcbb798f6d..5cb0fffddf1 100644 --- a/script/hassfest/quality_scale.py +++ b/script/hassfest/quality_scale.py @@ -983,7 +983,6 @@ INTEGRATIONS_WITHOUT_QUALITY_SCALE_FILE = [ "touchline", "touchline_sl", "tplink_lte", - "tplink_omada", "traccar", "traccar_server", "tractive", diff --git a/tests/components/tplink_omada/conftest.py b/tests/components/tplink_omada/conftest.py index 45f801e9827..b876a096a10 100644 --- a/tests/components/tplink_omada/conftest.py +++ b/tests/components/tplink_omada/conftest.py @@ -86,6 +86,7 @@ async def mock_omada_site_client(hass: HomeAssistant) -> AsyncGenerator[AsyncMoc site_client.get_known_clients.return_value = async_empty() site_client.get_connected_clients.return_value = async_empty() + site_client.reconnect_client = AsyncMock() return site_client @@ -159,6 +160,7 @@ def mock_omada_client(mock_omada_site_client: AsyncMock) -> Generator[MagicMock] client = client_mock.return_value client.get_site_client.return_value = mock_omada_site_client + client.login = AsyncMock() yield client diff --git a/tests/components/tplink_omada/test_init.py b/tests/components/tplink_omada/test_init.py index 762168df9d6..446ea71b427 100644 --- a/tests/components/tplink_omada/test_init.py +++ b/tests/components/tplink_omada/test_init.py @@ -2,8 +2,17 @@ from unittest.mock import MagicMock +import pytest +from tplink_omada_client.exceptions import ( + ConnectionFailed, + OmadaClientException, + UnsupportedControllerVersion, +) + from homeassistant.components.tplink_omada.const import DOMAIN +from homeassistant.config_entries import ConfigEntryState from homeassistant.core import HomeAssistant +from homeassistant.exceptions import HomeAssistantError, ServiceValidationError from homeassistant.helpers import device_registry as dr from tests.common import MockConfigEntry @@ -17,6 +26,41 @@ MOCK_ENTRY_DATA = { } +@pytest.mark.parametrize( + ("side_effect", "entry_state"), + [ + ( + UnsupportedControllerVersion("4.0.0"), + ConfigEntryState.SETUP_ERROR, + ), + ( + ConnectionFailed(), + ConfigEntryState.SETUP_RETRY, + ), + ( + OmadaClientException(), + ConfigEntryState.SETUP_RETRY, + ), + ], +) +async def test_setup_entry_login_failed_raises_configentryauthfailed( + hass: HomeAssistant, + mock_omada_client: MagicMock, + mock_config_entry: MockConfigEntry, + side_effect: OmadaClientException, + entry_state: ConfigEntryState, +) -> None: + """Test setup entry with login failed raises ConfigEntryAuthFailed.""" + mock_omada_client.login.side_effect = side_effect + + mock_config_entry.add_to_hass(hass) + + await hass.config_entries.async_setup(mock_config_entry.entry_id) + await hass.async_block_till_done() + + assert mock_config_entry.state == entry_state + + async def test_missing_devices_removed_at_startup( hass: HomeAssistant, device_registry: dr.DeviceRegistry, @@ -45,3 +89,73 @@ async def test_missing_devices_removed_at_startup( await hass.async_block_till_done() assert device_registry.async_get(device_entry.id) is None + + +async def test_service_reconnect_client( + hass: HomeAssistant, + mock_omada_site_client: MagicMock, + mock_omada_client: MagicMock, + mock_config_entry: MockConfigEntry, +) -> None: + """Test reconnect client service.""" + mock_config_entry.add_to_hass(hass) + + await hass.config_entries.async_setup(mock_config_entry.entry_id) + await hass.async_block_till_done() + + mac = "AA:BB:CC:DD:EE:FF" + await hass.services.async_call( + DOMAIN, + "reconnect_client", + {"mac": mac}, + blocking=True, + ) + + mock_omada_site_client.reconnect_client.assert_awaited_once_with(mac) + + +async def test_service_reconnect_failed_raises_servicevalidationerror( + hass: HomeAssistant, + mock_omada_site_client: MagicMock, + mock_omada_client: MagicMock, + mock_config_entry: MockConfigEntry, +) -> None: + """Test reconnect with missing mac address raises ServiceValidationError.""" + mock_config_entry.add_to_hass(hass) + + await hass.config_entries.async_setup(mock_config_entry.entry_id) + await hass.async_block_till_done() + + with pytest.raises(ServiceValidationError): + await hass.services.async_call( + DOMAIN, + "reconnect_client", + {}, + blocking=True, + ) + + +async def test_service_reconnect_failed_raises_homeassistanterror( + hass: HomeAssistant, + mock_omada_site_client: MagicMock, + mock_omada_client: MagicMock, + mock_config_entry: MockConfigEntry, +) -> None: + """Test reconnect client service raises the right kind of exception on service failure.""" + + mock_config_entry.add_to_hass(hass) + + await hass.config_entries.async_setup(mock_config_entry.entry_id) + await hass.async_block_till_done() + + mac = "AA:BB:CC:DD:EE:FF" + mock_omada_site_client.reconnect_client.side_effect = OmadaClientException + with pytest.raises(HomeAssistantError): + await hass.services.async_call( + DOMAIN, + "reconnect_client", + {"mac": mac}, + blocking=True, + ) + + mock_omada_site_client.reconnect_client.assert_awaited_once_with(mac)