mirror of
https://github.com/home-assistant/core.git
synced 2025-12-20 02:48:57 +00:00
Achieve Bronze quality rating for TP-Link Omada (#156697)
This commit is contained in:
@@ -14,7 +14,12 @@ from tplink_omada_client.exceptions import (
|
|||||||
from homeassistant.config_entries import ConfigEntry
|
from homeassistant.config_entries import ConfigEntry
|
||||||
from homeassistant.const import Platform
|
from homeassistant.const import Platform
|
||||||
from homeassistant.core import HomeAssistant, ServiceCall
|
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 homeassistant.helpers import device_registry as dr
|
||||||
|
|
||||||
from .config_flow import CONF_SITE, create_omada_client
|
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
|
entry.runtime_data = controller
|
||||||
|
|
||||||
async def handle_reconnect_client(call: ServiceCall) -> None:
|
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")
|
mac: str | None = call.data.get("mac")
|
||||||
if not 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)
|
hass.services.async_register(DOMAIN, "reconnect_client", handle_reconnect_client)
|
||||||
|
|
||||||
|
|||||||
80
homeassistant/components/tplink_omada/quality_scale.yaml
Normal file
80
homeassistant/components/tplink_omada/quality_scale.yaml
Normal 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
|
||||||
@@ -17,6 +17,10 @@
|
|||||||
"password": "[%key:common::config_flow::data::password%]",
|
"password": "[%key:common::config_flow::data::password%]",
|
||||||
"username": "[%key:common::config_flow::data::username%]"
|
"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.",
|
"description": "The provided credentials have stopped working. Please update them.",
|
||||||
"title": "Update TP-Link Omada credentials"
|
"title": "Update TP-Link Omada credentials"
|
||||||
},
|
},
|
||||||
@@ -24,7 +28,10 @@
|
|||||||
"data": {
|
"data": {
|
||||||
"site": "Site"
|
"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": {
|
"user": {
|
||||||
"data": {
|
"data": {
|
||||||
@@ -34,7 +41,10 @@
|
|||||||
"verify_ssl": "[%key:common::config_flow::data::verify_ssl%]"
|
"verify_ssl": "[%key:common::config_flow::data::verify_ssl%]"
|
||||||
},
|
},
|
||||||
"data_description": {
|
"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."
|
"description": "Enter the connection details for the Omada controller. Cloud controllers aren't supported."
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -983,7 +983,6 @@ INTEGRATIONS_WITHOUT_QUALITY_SCALE_FILE = [
|
|||||||
"touchline",
|
"touchline",
|
||||||
"touchline_sl",
|
"touchline_sl",
|
||||||
"tplink_lte",
|
"tplink_lte",
|
||||||
"tplink_omada",
|
|
||||||
"traccar",
|
"traccar",
|
||||||
"traccar_server",
|
"traccar_server",
|
||||||
"tractive",
|
"tractive",
|
||||||
|
|||||||
@@ -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_known_clients.return_value = async_empty()
|
||||||
site_client.get_connected_clients.return_value = async_empty()
|
site_client.get_connected_clients.return_value = async_empty()
|
||||||
|
site_client.reconnect_client = AsyncMock()
|
||||||
return site_client
|
return site_client
|
||||||
|
|
||||||
|
|
||||||
@@ -159,6 +160,7 @@ def mock_omada_client(mock_omada_site_client: AsyncMock) -> Generator[MagicMock]
|
|||||||
client = client_mock.return_value
|
client = client_mock.return_value
|
||||||
|
|
||||||
client.get_site_client.return_value = mock_omada_site_client
|
client.get_site_client.return_value = mock_omada_site_client
|
||||||
|
client.login = AsyncMock()
|
||||||
yield client
|
yield client
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -2,8 +2,17 @@
|
|||||||
|
|
||||||
from unittest.mock import MagicMock
|
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.components.tplink_omada.const import DOMAIN
|
||||||
|
from homeassistant.config_entries import ConfigEntryState
|
||||||
from homeassistant.core import HomeAssistant
|
from homeassistant.core import HomeAssistant
|
||||||
|
from homeassistant.exceptions import HomeAssistantError, ServiceValidationError
|
||||||
from homeassistant.helpers import device_registry as dr
|
from homeassistant.helpers import device_registry as dr
|
||||||
|
|
||||||
from tests.common import MockConfigEntry
|
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(
|
async def test_missing_devices_removed_at_startup(
|
||||||
hass: HomeAssistant,
|
hass: HomeAssistant,
|
||||||
device_registry: dr.DeviceRegistry,
|
device_registry: dr.DeviceRegistry,
|
||||||
@@ -45,3 +89,73 @@ async def test_missing_devices_removed_at_startup(
|
|||||||
await hass.async_block_till_done()
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
assert device_registry.async_get(device_entry.id) is None
|
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)
|
||||||
|
|||||||
Reference in New Issue
Block a user