1
0
mirror of https://github.com/home-assistant/core.git synced 2025-12-19 18:38:58 +00:00

Achieve Bronze quality rating for TP-Link Omada (#156697)

This commit is contained in:
MarkGodwin
2025-12-18 18:44:08 +00:00
committed by GitHub
parent e721c1a092
commit 4b4b64e939
6 changed files with 220 additions and 7 deletions

View File

@@ -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)

View File

@@ -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

View File

@@ -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."
}

View File

@@ -983,7 +983,6 @@ INTEGRATIONS_WITHOUT_QUALITY_SCALE_FILE = [
"touchline",
"touchline_sl",
"tplink_lte",
"tplink_omada",
"traccar",
"traccar_server",
"tractive",

View File

@@ -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

View File

@@ -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)