Fix ruckus_unleashed for python 3.11 (#94835)

Co-authored-by: Tony <29752086+ms264556@users.noreply.github.com>
This commit is contained in:
Ian Foster 2023-08-28 17:26:40 +02:00 committed by GitHub
parent 739eeeccb0
commit ef7a246f09
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 430 additions and 299 deletions

View File

@ -1057,8 +1057,8 @@ build.json @home-assistant/supervisor
/tests/components/rss_feed_template/ @home-assistant/core
/homeassistant/components/rtsp_to_webrtc/ @allenporter
/tests/components/rtsp_to_webrtc/ @allenporter
/homeassistant/components/ruckus_unleashed/ @gabe565
/tests/components/ruckus_unleashed/ @gabe565
/homeassistant/components/ruckus_unleashed/ @gabe565 @lanrat
/tests/components/ruckus_unleashed/ @gabe565 @lanrat
/homeassistant/components/ruuvi_gateway/ @akx
/tests/components/ruuvi_gateway/ @akx
/homeassistant/components/ruuvitag_ble/ @akx

View File

@ -1,21 +1,22 @@
"""The Ruckus Unleashed integration."""
import logging
from pyruckus import Ruckus
from aioruckus import AjaxSession
from aioruckus.exceptions import AuthenticationError
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ConfigEntryNotReady
from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady
from homeassistant.helpers import device_registry as dr
from .const import (
API_AP,
API_DEVICE_NAME,
API_ID,
API_MAC,
API_MODEL,
API_SYSTEM_OVERVIEW,
API_VERSION,
API_AP_DEVNAME,
API_AP_FIRMWAREVERSION,
API_AP_MAC,
API_AP_MODEL,
API_SYS_SYSINFO,
API_SYS_SYSINFO_VERSION,
COORDINATOR,
DOMAIN,
MANUFACTURER,
@ -24,35 +25,45 @@ from .const import (
)
from .coordinator import RuckusUnleashedDataUpdateCoordinator
_LOGGER = logging.getLogger(__package__)
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Set up Ruckus Unleashed from a config entry."""
try:
ruckus = await Ruckus.create(
ruckus = AjaxSession.async_create(
entry.data[CONF_HOST],
entry.data[CONF_USERNAME],
entry.data[CONF_PASSWORD],
)
except ConnectionError as error:
raise ConfigEntryNotReady from error
await ruckus.login()
except (ConnectionRefusedError, ConnectionError) as conerr:
raise ConfigEntryNotReady from conerr
except AuthenticationError as autherr:
raise ConfigEntryAuthFailed from autherr
coordinator = RuckusUnleashedDataUpdateCoordinator(hass, ruckus=ruckus)
await coordinator.async_config_entry_first_refresh()
system_info = await ruckus.system_info()
system_info = await ruckus.api.get_system_info()
registry = dr.async_get(hass)
ap_info = await ruckus.ap_info()
for device in ap_info[API_AP][API_ID].values():
aps = await ruckus.api.get_aps()
for access_point in aps:
_LOGGER.debug("AP [%s] %s", access_point[API_AP_MAC], entry.entry_id)
registry.async_get_or_create(
config_entry_id=entry.entry_id,
connections={(dr.CONNECTION_NETWORK_MAC, device[API_MAC])},
identifiers={(dr.CONNECTION_NETWORK_MAC, device[API_MAC])},
connections={(dr.CONNECTION_NETWORK_MAC, access_point[API_AP_MAC])},
identifiers={(DOMAIN, access_point[API_AP_MAC])},
manufacturer=MANUFACTURER,
name=device[API_DEVICE_NAME],
model=device[API_MODEL],
sw_version=system_info[API_SYSTEM_OVERVIEW][API_VERSION],
name=access_point[API_AP_DEVNAME],
model=access_point[API_AP_MODEL],
sw_version=access_point.get(
API_AP_FIRMWAREVERSION,
system_info[API_SYS_SYSINFO][API_SYS_SYSINFO_VERSION],
),
)
hass.data.setdefault(DOMAIN, {})
@ -68,11 +79,12 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Unload a config entry."""
unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
if unload_ok:
for listener in hass.data[DOMAIN][entry.entry_id][UNDO_UPDATE_LISTENERS]:
listener()
await hass.data[DOMAIN][entry.entry_id][COORDINATOR].ruckus.close()
hass.data[DOMAIN].pop(entry.entry_id)
return unload_ok

View File

@ -1,22 +1,29 @@
"""Config flow for Ruckus Unleashed integration."""
import logging
from collections.abc import Mapping
from typing import Any
from pyruckus import Ruckus
from pyruckus.exceptions import AuthenticationError
from aioruckus import AjaxSession, SystemStat
from aioruckus.exceptions import AuthenticationError
import voluptuous as vol
from homeassistant import config_entries, core, exceptions
from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME
from homeassistant.data_entry_flow import FlowResult
from .const import API_SERIAL, API_SYSTEM_OVERVIEW, DOMAIN
_LOGGER = logging.getLogger(__package__)
from .const import (
API_MESH_NAME,
API_SYS_SYSINFO,
API_SYS_SYSINFO_SERIAL,
DOMAIN,
KEY_SYS_SERIAL,
KEY_SYS_TITLE,
)
DATA_SCHEMA = vol.Schema(
{
vol.Required("host"): str,
vol.Required("username"): str,
vol.Required("password"): str,
vol.Required(CONF_HOST): str,
vol.Required(CONF_USERNAME): str,
vol.Required(CONF_PASSWORD): str,
}
)
@ -28,26 +35,22 @@ async def validate_input(hass: core.HomeAssistant, data):
"""
try:
ruckus = await Ruckus.create(
async with AjaxSession.async_create(
data[CONF_HOST], data[CONF_USERNAME], data[CONF_PASSWORD]
)
except AuthenticationError as error:
raise InvalidAuth from error
except ConnectionError as error:
raise CannotConnect from error
mesh_name = await ruckus.mesh_name()
system_info = await ruckus.system_info()
try:
host_serial = system_info[API_SYSTEM_OVERVIEW][API_SERIAL]
except KeyError as error:
raise CannotConnect from error
return {
"title": mesh_name,
"serial": host_serial,
}
) as ruckus:
system_info = await ruckus.api.get_system_info(
SystemStat.SYSINFO,
)
mesh_name = (await ruckus.api.get_mesh_info())[API_MESH_NAME]
zd_serial = system_info[API_SYS_SYSINFO][API_SYS_SYSINFO_SERIAL]
return {
KEY_SYS_TITLE: mesh_name,
KEY_SYS_SERIAL: zd_serial,
}
except AuthenticationError as autherr:
raise InvalidAuth from autherr
except (ConnectionRefusedError, ConnectionError, KeyError) as connerr:
raise CannotConnect from connerr
class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
@ -55,7 +58,9 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
VERSION = 1
async def async_step_user(self, user_input=None):
async def async_step_user(
self, user_input: dict[str, Any] | None = None
) -> FlowResult:
"""Handle the initial step."""
errors = {}
if user_input is not None:
@ -65,18 +70,32 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
errors["base"] = "cannot_connect"
except InvalidAuth:
errors["base"] = "invalid_auth"
except Exception: # pylint: disable=broad-except
_LOGGER.exception("Unexpected exception")
errors["base"] = "unknown"
else:
await self.async_set_unique_id(info["serial"])
await self.async_set_unique_id(info[KEY_SYS_SERIAL])
self._abort_if_unique_id_configured()
return self.async_create_entry(title=info["title"], data=user_input)
return self.async_create_entry(
title=info[KEY_SYS_TITLE], data=user_input
)
return self.async_show_form(
step_id="user", data_schema=DATA_SCHEMA, errors=errors
)
async def async_step_reauth(self, entry_data: Mapping[str, Any]) -> FlowResult:
"""Perform reauth upon an API authentication error."""
return await self.async_step_reauth_confirm()
async def async_step_reauth_confirm(
self, user_input: dict[str, Any] | None = None
) -> FlowResult:
"""Dialog that informs the user that reauth is required."""
if user_input is None:
return self.async_show_form(
step_id="reauth_confirm",
data_schema=DATA_SCHEMA,
)
return await self.async_step_user()
class CannotConnect(exceptions.HomeAssistantError):
"""Error to indicate we cannot connect."""

View File

@ -3,23 +3,35 @@ from homeassistant.const import Platform
DOMAIN = "ruckus_unleashed"
PLATFORMS = [Platform.DEVICE_TRACKER]
SCAN_INTERVAL = 180
SCAN_INTERVAL = 30
MANUFACTURER = "Ruckus"
COORDINATOR = "coordinator"
UNDO_UPDATE_LISTENERS = "undo_update_listeners"
API_CLIENTS = "clients"
API_NAME = "host_name"
API_MAC = "mac_address"
API_IP = "user_ip"
API_SYSTEM_OVERVIEW = "system_overview"
API_SERIAL = "serial_number"
API_DEVICE_NAME = "device_name"
API_MODEL = "model"
API_VERSION = "version"
API_AP = "ap"
API_ID = "id"
API_CURRENT_ACTIVE_CLIENTS = "current_active_clients"
API_ACCESS_POINT = "access_point"
KEY_SYS_CLIENTS = "clients"
KEY_SYS_TITLE = "title"
KEY_SYS_SERIAL = "serial"
API_MESH_NAME = "name"
API_MESH_PSK = "psk"
API_CLIENT_HOSTNAME = "hostname"
API_CLIENT_MAC = "mac"
API_CLIENT_IP = "ip"
API_CLIENT_AP_MAC = "ap"
API_AP_MAC = "mac"
API_AP_SERIALNUMBER = "serial"
API_AP_DEVNAME = "devname"
API_AP_MODEL = "model"
API_AP_FIRMWAREVERSION = "version"
API_SYS_SYSINFO = "sysinfo"
API_SYS_SYSINFO_VERSION = "version"
API_SYS_SYSINFO_SERIAL = "serial"
API_SYS_IDENTITY = "identity"
API_SYS_IDENTITY_NAME = "name"
API_SYS_UNLEASHEDNETWORK = "unleashed-network"
API_SYS_UNLEASHEDNETWORK_TOKEN = "unleashed-network-token"

View File

@ -2,19 +2,13 @@
from datetime import timedelta
import logging
from pyruckus import Ruckus
from pyruckus.exceptions import AuthenticationError
from aioruckus import AjaxSession
from aioruckus.exceptions import AuthenticationError
from homeassistant.core import HomeAssistant
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
from .const import (
API_CLIENTS,
API_CURRENT_ACTIVE_CLIENTS,
API_MAC,
DOMAIN,
SCAN_INTERVAL,
)
from .const import API_CLIENT_MAC, DOMAIN, KEY_SYS_CLIENTS, SCAN_INTERVAL
_LOGGER = logging.getLogger(__package__)
@ -22,7 +16,7 @@ _LOGGER = logging.getLogger(__package__)
class RuckusUnleashedDataUpdateCoordinator(DataUpdateCoordinator):
"""Coordinator to manage data from Ruckus Unleashed client."""
def __init__(self, hass: HomeAssistant, *, ruckus: Ruckus) -> None:
def __init__(self, hass: HomeAssistant, *, ruckus: AjaxSession) -> None:
"""Initialize global Ruckus Unleashed data updater."""
self.ruckus = ruckus
@ -37,12 +31,15 @@ class RuckusUnleashedDataUpdateCoordinator(DataUpdateCoordinator):
async def _fetch_clients(self) -> dict:
"""Fetch clients from the API and format them."""
clients = await self.ruckus.current_active_clients()
return {e[API_MAC]: e for e in clients[API_CURRENT_ACTIVE_CLIENTS][API_CLIENTS]}
clients = await self.ruckus.api.get_active_clients()
_LOGGER.debug("fetched %d active clients", len(clients))
return {client[API_CLIENT_MAC]: client for client in clients}
async def _async_update_data(self) -> dict:
"""Fetch Ruckus Unleashed data."""
try:
return {API_CLIENTS: await self._fetch_clients()}
except (AuthenticationError, ConnectionError) as error:
raise UpdateFailed(error) from error
return {KEY_SYS_CLIENTS: await self._fetch_clients()}
except AuthenticationError as autherror:
raise UpdateFailed(autherror) from autherror
except (ConnectionRefusedError, ConnectionError) as conerr:
raise UpdateFailed(conerr) from conerr

View File

@ -1,6 +1,8 @@
"""Support for Ruckus Unleashed devices."""
from __future__ import annotations
import logging
from homeassistant.components.device_tracker import ScannerEntity, SourceType
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant, callback
@ -9,14 +11,16 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.update_coordinator import CoordinatorEntity
from .const import (
API_CLIENTS,
API_NAME,
API_CLIENT_HOSTNAME,
API_CLIENT_IP,
COORDINATOR,
DOMAIN,
MANUFACTURER,
KEY_SYS_CLIENTS,
UNDO_UPDATE_LISTENERS,
)
_LOGGER = logging.getLogger(__package__)
async def async_setup_entry(
hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback
@ -46,12 +50,15 @@ def add_new_entities(coordinator, async_add_entities, tracked):
"""Add new tracker entities from the router."""
new_tracked = []
for mac in coordinator.data[API_CLIENTS]:
for mac in coordinator.data[KEY_SYS_CLIENTS]:
if mac in tracked:
continue
device = coordinator.data[API_CLIENTS][mac]
new_tracked.append(RuckusUnleashedDevice(coordinator, mac, device[API_NAME]))
device = coordinator.data[KEY_SYS_CLIENTS][mac]
_LOGGER.debug("adding new device: [%s] %s", mac, device[API_CLIENT_HOSTNAME])
new_tracked.append(
RuckusUnleashedDevice(coordinator, mac, device[API_CLIENT_HOSTNAME])
)
tracked.add(mac)
async_add_entities(new_tracked)
@ -66,7 +73,7 @@ def restore_entities(registry, coordinator, entry, async_add_entities, tracked):
if (
entity.config_entry_id == entry.entry_id
and entity.platform == DOMAIN
and entity.unique_id not in coordinator.data[API_CLIENTS]
and entity.unique_id not in coordinator.data[KEY_SYS_CLIENTS]
):
missing.append(
RuckusUnleashedDevice(
@ -75,6 +82,7 @@ def restore_entities(registry, coordinator, entry, async_add_entities, tracked):
)
tracked.add(entity.unique_id)
_LOGGER.debug("added %d missing devices", len(missing))
async_add_entities(missing)
@ -95,17 +103,25 @@ class RuckusUnleashedDevice(CoordinatorEntity, ScannerEntity):
@property
def name(self) -> str:
"""Return the name."""
if self.is_connected:
return (
self.coordinator.data[API_CLIENTS][self._mac][API_NAME]
or f"{MANUFACTURER} {self._mac}"
)
return self._name
return (
self._name
if not self.is_connected
else self.coordinator.data[KEY_SYS_CLIENTS][self._mac][API_CLIENT_HOSTNAME]
)
@property
def ip_address(self) -> str:
"""Return the ip address."""
return (
self.coordinator.data[KEY_SYS_CLIENTS][self._mac][API_CLIENT_IP]
if self.is_connected
else None
)
@property
def is_connected(self) -> bool:
"""Return true if the device is connected to the network."""
return self._mac in self.coordinator.data[API_CLIENTS]
return self._mac in self.coordinator.data[KEY_SYS_CLIENTS]
@property
def source_type(self) -> SourceType:

View File

@ -1,10 +1,11 @@
{
"domain": "ruckus_unleashed",
"name": "Ruckus Unleashed",
"codeowners": ["@gabe565"],
"codeowners": ["@gabe565", "@lanrat"],
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/ruckus_unleashed",
"integration_type": "hub",
"iot_class": "local_polling",
"loggers": ["pexpect", "pyruckus"],
"requirements": ["pyruckus==0.16"]
"loggers": ["aioruckus", "xmltodict"],
"requirements": ["aioruckus==0.31", "xmltodict==0.13.0"]
}

View File

@ -332,6 +332,9 @@ aiorecollect==1.0.8
# homeassistant.components.ridwell
aioridwell==2023.07.0
# homeassistant.components.ruckus_unleashed
aioruckus==0.31
# homeassistant.components.ruuvi_gateway
aioruuvigateway==0.1.0
@ -1975,9 +1978,6 @@ pyrituals==0.0.6
# homeassistant.components.thread
pyroute2==0.7.5
# homeassistant.components.ruckus_unleashed
pyruckus==0.16
# homeassistant.components.rympro
pyrympro==0.0.7
@ -2719,6 +2719,7 @@ xknxproject==3.2.0
# homeassistant.components.bluesound
# homeassistant.components.fritz
# homeassistant.components.rest
# homeassistant.components.ruckus_unleashed
# homeassistant.components.startca
# homeassistant.components.ted5000
# homeassistant.components.zestimate

View File

@ -307,6 +307,9 @@ aiorecollect==1.0.8
# homeassistant.components.ridwell
aioridwell==2023.07.0
# homeassistant.components.ruckus_unleashed
aioruckus==0.31
# homeassistant.components.ruuvi_gateway
aioruuvigateway==0.1.0
@ -1467,9 +1470,6 @@ pyrituals==0.0.6
# homeassistant.components.thread
pyroute2==0.7.5
# homeassistant.components.ruckus_unleashed
pyruckus==0.16
# homeassistant.components.rympro
pyrympro==0.0.7
@ -2001,6 +2001,7 @@ xknxproject==3.2.0
# homeassistant.components.bluesound
# homeassistant.components.fritz
# homeassistant.components.rest
# homeassistant.components.ruckus_unleashed
# homeassistant.components.startca
# homeassistant.components.ted5000
# homeassistant.components.zestimate

View File

@ -1,45 +1,61 @@
"""Tests for the Ruckus Unleashed integration."""
from unittest.mock import patch
from unittest.mock import AsyncMock, patch
from aioruckus import AjaxSession, RuckusAjaxApi
from homeassistant.components.ruckus_unleashed import DOMAIN
from homeassistant.components.ruckus_unleashed.const import (
API_ACCESS_POINT,
API_AP,
API_DEVICE_NAME,
API_ID,
API_IP,
API_MAC,
API_MODEL,
API_NAME,
API_SERIAL,
API_SYSTEM_OVERVIEW,
API_VERSION,
API_AP_DEVNAME,
API_AP_MAC,
API_AP_MODEL,
API_AP_SERIALNUMBER,
API_CLIENT_AP_MAC,
API_CLIENT_HOSTNAME,
API_CLIENT_IP,
API_CLIENT_MAC,
API_MESH_NAME,
API_MESH_PSK,
API_SYS_IDENTITY,
API_SYS_IDENTITY_NAME,
API_SYS_SYSINFO,
API_SYS_SYSINFO_SERIAL,
API_SYS_SYSINFO_VERSION,
API_SYS_UNLEASHEDNETWORK,
API_SYS_UNLEASHEDNETWORK_TOKEN,
DOMAIN,
)
from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME
from homeassistant.core import HomeAssistant
from homeassistant.helpers import device_registry as dr
from tests.common import MockConfigEntry
DEFAULT_TITLE = "Ruckus Mesh"
DEFAULT_UNIQUE_ID = "123456789012"
DEFAULT_SYSTEM_INFO = {
API_SYSTEM_OVERVIEW: {
API_SERIAL: DEFAULT_UNIQUE_ID,
API_VERSION: "v1.0.0",
}
API_SYS_IDENTITY: {API_SYS_IDENTITY_NAME: "RuckusUnleashed"},
API_SYS_SYSINFO: {
API_SYS_SYSINFO_SERIAL: "123456789012",
API_SYS_SYSINFO_VERSION: "200.7.10.202 build 141",
},
API_SYS_UNLEASHEDNETWORK: {
API_SYS_UNLEASHEDNETWORK_TOKEN: "un1234567890121680060227001"
},
}
DEFAULT_AP_INFO = {
API_AP: {
API_ID: {
"1": {
API_MAC: "00:11:22:33:44:55",
API_DEVICE_NAME: "Test Device",
API_MODEL: "r510",
}
}
}
DEFAULT_MESH_INFO = {
API_MESH_NAME: "Ruckus Mesh",
API_MESH_PSK: "",
}
DEFAULT_AP_INFO = [
{
API_AP_MAC: "00:11:22:33:44:55",
API_AP_DEVNAME: "Test Device",
API_AP_MODEL: "r510",
API_AP_SERIALNUMBER: DEFAULT_SYSTEM_INFO[API_SYS_SYSINFO][
API_SYS_SYSINFO_SERIAL
],
}
]
CONFIG = {
CONF_HOST: "1.1.1.1",
CONF_USERNAME: "test-username",
@ -48,25 +64,28 @@ CONFIG = {
TEST_CLIENT_ENTITY_ID = "device_tracker.ruckus_test_device"
TEST_CLIENT = {
API_IP: "1.1.1.2",
API_MAC: "AA:BB:CC:DD:EE:FF",
API_NAME: "Ruckus Test Device",
API_ACCESS_POINT: "00:11:22:33:44:55",
API_CLIENT_IP: "1.1.1.2",
API_CLIENT_MAC: "AA:BB:CC:DD:EE:FF",
API_CLIENT_HOSTNAME: "Ruckus Test Device",
API_CLIENT_AP_MAC: DEFAULT_AP_INFO[0][API_AP_MAC],
}
DEFAULT_TITLE = DEFAULT_MESH_INFO[API_MESH_NAME]
DEFAULT_UNIQUEID = DEFAULT_SYSTEM_INFO[API_SYS_SYSINFO][API_SYS_SYSINFO_SERIAL]
def mock_config_entry() -> MockConfigEntry:
"""Return a Ruckus Unleashed mock config entry."""
return MockConfigEntry(
domain=DOMAIN,
title=DEFAULT_TITLE,
unique_id=DEFAULT_UNIQUE_ID,
unique_id=DEFAULT_UNIQUEID,
data=CONFIG,
options=None,
)
async def init_integration(hass) -> MockConfigEntry:
async def init_integration(hass: HomeAssistant) -> MockConfigEntry:
"""Set up the Ruckus Unleashed integration in Home Assistant."""
entry = mock_config_entry()
entry.add_to_hass(hass)
@ -76,27 +95,103 @@ async def init_integration(hass) -> MockConfigEntry:
dr.async_get(hass).async_get_or_create(
name="Device from other integration",
config_entry_id=other_config_entry.entry_id,
connections={(dr.CONNECTION_NETWORK_MAC, TEST_CLIENT[API_MAC])},
connections={(dr.CONNECTION_NETWORK_MAC, TEST_CLIENT[API_CLIENT_MAC])},
)
with patch(
"homeassistant.components.ruckus_unleashed.Ruckus.connect",
return_value=None,
), patch(
"homeassistant.components.ruckus_unleashed.Ruckus.mesh_name",
return_value=DEFAULT_TITLE,
), patch(
"homeassistant.components.ruckus_unleashed.Ruckus.system_info",
return_value=DEFAULT_SYSTEM_INFO,
), patch(
"homeassistant.components.ruckus_unleashed.Ruckus.ap_info",
return_value=DEFAULT_AP_INFO,
), patch(
"homeassistant.components.ruckus_unleashed.RuckusUnleashedDataUpdateCoordinator._fetch_clients",
return_value={
TEST_CLIENT[API_MAC]: TEST_CLIENT,
},
):
with RuckusAjaxApiPatchContext():
await hass.config_entries.async_setup(entry.entry_id)
await hass.async_block_till_done()
return entry
class RuckusAjaxApiPatchContext:
"""Context Manager which mocks the Ruckus AjaxSession and RuckusAjaxApi."""
def __init__(
self,
login_mock: AsyncMock = None,
system_info: dict | None = None,
mesh_info: dict | None = None,
active_clients: list[dict] | AsyncMock | None = None,
) -> None:
"""Initialize Ruckus Mock Context Manager."""
self.login_mock = login_mock
self.system_info = system_info
self.mesh_info = mesh_info
self.active_clients = active_clients
self.patchers = []
def __enter__(self):
"""Patch RuckusAjaxApi and AjaxSession methods."""
self.patchers.append(
patch.object(RuckusAjaxApi, "_get_conf", new=AsyncMock(return_value={}))
)
self.patchers.append(
patch.object(
RuckusAjaxApi, "get_aps", new=AsyncMock(return_value=DEFAULT_AP_INFO)
)
)
self.patchers.append(
patch.object(
RuckusAjaxApi,
"get_system_info",
new=AsyncMock(
return_value=DEFAULT_SYSTEM_INFO
if self.system_info is None
else self.system_info
),
)
)
self.patchers.append(
patch.object(
RuckusAjaxApi,
"get_mesh_info",
new=AsyncMock(
return_value=DEFAULT_MESH_INFO
if self.mesh_info is None
else self.mesh_info
),
)
)
self.patchers.append(
patch.object(
RuckusAjaxApi,
"get_active_clients",
new=self.active_clients
if isinstance(self.active_clients, AsyncMock)
else AsyncMock(
return_value=[TEST_CLIENT]
if self.active_clients is None
else self.active_clients
),
)
)
self.patchers.append(
patch.object(
AjaxSession,
"login",
new=self.login_mock or AsyncMock(return_value=self),
)
)
self.patchers.append(
patch.object(AjaxSession, "close", new=AsyncMock(return_value=None))
)
def _patched_async_create(
host: str, username: str, password: str
) -> "AjaxSession":
return AjaxSession(None, host, username, password)
self.patchers.append(
patch.object(AjaxSession, "async_create", new=_patched_async_create)
)
for patcher in self.patchers:
patcher.start()
return self
def __exit__(self, exc_type, exc_val, exc_tb):
"""Remove RuckusAjaxApi and AjaxSession patches."""
for patcher in self.patchers:
patcher.stop()

View File

@ -1,15 +1,21 @@
"""Test the Ruckus Unleashed config flow."""
from datetime import timedelta
from unittest.mock import patch
from unittest.mock import AsyncMock, patch
from pyruckus.exceptions import AuthenticationError
from aioruckus.const import (
ERROR_CONNECT_TEMPORARY,
ERROR_CONNECT_TIMEOUT,
ERROR_LOGIN_INCORRECT,
)
from aioruckus.exceptions import AuthenticationError
from homeassistant import config_entries
from homeassistant import config_entries, data_entry_flow
from homeassistant.components.ruckus_unleashed.const import DOMAIN
from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME
from homeassistant.core import HomeAssistant
from homeassistant.util import utcnow
from . import CONFIG, DEFAULT_SYSTEM_INFO, DEFAULT_TITLE
from . import CONFIG, DEFAULT_TITLE, RuckusAjaxApiPatchContext, mock_config_entry
from tests.common import async_fire_time_changed
@ -22,16 +28,7 @@ async def test_form(hass: HomeAssistant) -> None:
assert result["type"] == "form"
assert result["errors"] == {}
with patch(
"homeassistant.components.ruckus_unleashed.Ruckus.connect",
return_value=None,
), patch(
"homeassistant.components.ruckus_unleashed.Ruckus.mesh_name",
return_value=DEFAULT_TITLE,
), patch(
"homeassistant.components.ruckus_unleashed.Ruckus.system_info",
return_value=DEFAULT_SYSTEM_INFO,
), patch(
with RuckusAjaxApiPatchContext(), patch(
"homeassistant.components.ruckus_unleashed.async_setup_entry",
return_value=True,
) as mock_setup_entry:
@ -41,10 +38,10 @@ async def test_form(hass: HomeAssistant) -> None:
)
await hass.async_block_till_done()
assert result2["type"] == "create_entry"
assert result2["title"] == DEFAULT_TITLE
assert result2["data"] == CONFIG
assert len(mock_setup_entry.mock_calls) == 1
assert result2["type"] == "create_entry"
assert result2["title"] == DEFAULT_TITLE
assert result2["data"] == CONFIG
assert len(mock_setup_entry.mock_calls) == 1
async def test_form_invalid_auth(hass: HomeAssistant) -> None:
@ -53,9 +50,8 @@ async def test_form_invalid_auth(hass: HomeAssistant) -> None:
DOMAIN, context={"source": config_entries.SOURCE_USER}
)
with patch(
"homeassistant.components.ruckus_unleashed.Ruckus.connect",
side_effect=AuthenticationError,
with RuckusAjaxApiPatchContext(
login_mock=AsyncMock(side_effect=AuthenticationError(ERROR_LOGIN_INCORRECT))
):
result2 = await hass.config_entries.flow.async_configure(
result["flow_id"],
@ -66,15 +62,44 @@ async def test_form_invalid_auth(hass: HomeAssistant) -> None:
assert result2["errors"] == {"base": "invalid_auth"}
async def test_form_user_reauth(hass: HomeAssistant) -> None:
"""Test reauth."""
entry = mock_config_entry()
entry.add_to_hass(hass)
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": config_entries.SOURCE_REAUTH}
)
flows = hass.config_entries.flow.async_progress()
assert len(flows) == 1
assert "flow_id" in flows[0]
assert result["type"] == data_entry_flow.FlowResultType.FORM
assert result["step_id"] == "reauth_confirm"
result2 = await hass.config_entries.flow.async_configure(
flows[0]["flow_id"],
user_input={
CONF_HOST: "1.2.3.4",
CONF_USERNAME: "new_name",
CONF_PASSWORD: "new_pass",
},
)
await hass.async_block_till_done()
assert result2["type"] == data_entry_flow.FlowResultType.FORM
assert result2["step_id"] == "user"
async def test_form_cannot_connect(hass: HomeAssistant) -> None:
"""Test we handle cannot connect error."""
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": config_entries.SOURCE_USER}
)
with patch(
"homeassistant.components.ruckus_unleashed.Ruckus.connect",
side_effect=ConnectionError,
with RuckusAjaxApiPatchContext(
login_mock=AsyncMock(side_effect=ConnectionError(ERROR_CONNECT_TIMEOUT))
):
result2 = await hass.config_entries.flow.async_configure(
result["flow_id"],
@ -85,15 +110,16 @@ async def test_form_cannot_connect(hass: HomeAssistant) -> None:
assert result2["errors"] == {"base": "cannot_connect"}
async def test_form_unknown_error(hass: HomeAssistant) -> None:
async def test_form_unexpected_response(hass: HomeAssistant) -> None:
"""Test we handle unknown error."""
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": config_entries.SOURCE_USER}
)
with patch(
"homeassistant.components.ruckus_unleashed.Ruckus.connect",
side_effect=Exception,
with RuckusAjaxApiPatchContext(
login_mock=AsyncMock(
side_effect=ConnectionRefusedError(ERROR_CONNECT_TEMPORARY)
)
):
result2 = await hass.config_entries.flow.async_configure(
result["flow_id"],
@ -101,7 +127,7 @@ async def test_form_unknown_error(hass: HomeAssistant) -> None:
)
assert result2["type"] == "form"
assert result2["errors"] == {"base": "unknown"}
assert result2["errors"] == {"base": "cannot_connect"}
async def test_form_cannot_connect_unknown_serial(hass: HomeAssistant) -> None:
@ -112,16 +138,7 @@ async def test_form_cannot_connect_unknown_serial(hass: HomeAssistant) -> None:
assert result["type"] == "form"
assert result["errors"] == {}
with patch(
"homeassistant.components.ruckus_unleashed.Ruckus.connect",
return_value=None,
), patch(
"homeassistant.components.ruckus_unleashed.Ruckus.mesh_name",
return_value=DEFAULT_TITLE,
), patch(
"homeassistant.components.ruckus_unleashed.Ruckus.system_info",
return_value={},
):
with RuckusAjaxApiPatchContext(system_info={}):
result2 = await hass.config_entries.flow.async_configure(
result["flow_id"],
CONFIG,
@ -137,16 +154,7 @@ async def test_form_duplicate_error(hass: HomeAssistant) -> None:
DOMAIN, context={"source": config_entries.SOURCE_USER}
)
with patch(
"homeassistant.components.ruckus_unleashed.Ruckus.connect",
return_value=None,
), patch(
"homeassistant.components.ruckus_unleashed.Ruckus.mesh_name",
return_value=DEFAULT_TITLE,
), patch(
"homeassistant.components.ruckus_unleashed.Ruckus.system_info",
return_value=DEFAULT_SYSTEM_INFO,
):
with RuckusAjaxApiPatchContext():
await hass.config_entries.flow.async_configure(
result["flow_id"],
CONFIG,

View File

@ -1,8 +1,11 @@
"""The sensor tests for the Ruckus Unleashed platform."""
from datetime import timedelta
from unittest.mock import patch
from unittest.mock import AsyncMock
from homeassistant.components.ruckus_unleashed import API_MAC, DOMAIN
from aioruckus.const import ERROR_CONNECT_EOF, ERROR_LOGIN_INCORRECT
from aioruckus.exceptions import AuthenticationError
from homeassistant.components.ruckus_unleashed import DOMAIN
from homeassistant.const import STATE_HOME, STATE_NOT_HOME, STATE_UNAVAILABLE
from homeassistant.core import HomeAssistant
from homeassistant.helpers import entity_registry as er
@ -10,12 +13,9 @@ from homeassistant.helpers.entity_component import async_update_entity
from homeassistant.util import utcnow
from . import (
DEFAULT_AP_INFO,
DEFAULT_SYSTEM_INFO,
DEFAULT_TITLE,
DEFAULT_UNIQUE_ID,
TEST_CLIENT,
DEFAULT_UNIQUEID,
TEST_CLIENT_ENTITY_ID,
RuckusAjaxApiPatchContext,
init_integration,
mock_config_entry,
)
@ -28,12 +28,7 @@ async def test_client_connected(hass: HomeAssistant) -> None:
await init_integration(hass)
future = utcnow() + timedelta(minutes=60)
with patch(
"homeassistant.components.ruckus_unleashed.RuckusUnleashedDataUpdateCoordinator._fetch_clients",
return_value={
TEST_CLIENT[API_MAC]: TEST_CLIENT,
},
):
with RuckusAjaxApiPatchContext():
async_fire_time_changed(hass, future)
await hass.async_block_till_done()
await async_update_entity(hass, TEST_CLIENT_ENTITY_ID)
@ -47,10 +42,7 @@ async def test_client_disconnected(hass: HomeAssistant) -> None:
await init_integration(hass)
future = utcnow() + timedelta(minutes=60)
with patch(
"homeassistant.components.ruckus_unleashed.RuckusUnleashedDataUpdateCoordinator._fetch_clients",
return_value={},
):
with RuckusAjaxApiPatchContext(active_clients={}):
async_fire_time_changed(hass, future)
await hass.async_block_till_done()
@ -64,9 +56,24 @@ async def test_clients_update_failed(hass: HomeAssistant) -> None:
await init_integration(hass)
future = utcnow() + timedelta(minutes=60)
with patch(
"homeassistant.components.ruckus_unleashed.RuckusUnleashedDataUpdateCoordinator._fetch_clients",
side_effect=ConnectionError,
with RuckusAjaxApiPatchContext(
active_clients=AsyncMock(side_effect=ConnectionError(ERROR_CONNECT_EOF))
):
async_fire_time_changed(hass, future)
await hass.async_block_till_done()
await async_update_entity(hass, TEST_CLIENT_ENTITY_ID)
test_client = hass.states.get(TEST_CLIENT_ENTITY_ID)
assert test_client.state == STATE_UNAVAILABLE
async def test_clients_update_auth_failed(hass: HomeAssistant) -> None:
"""Test failed update with bad auth."""
await init_integration(hass)
future = utcnow() + timedelta(minutes=60)
with RuckusAjaxApiPatchContext(
active_clients=AsyncMock(side_effect=AuthenticationError(ERROR_LOGIN_INCORRECT))
):
async_fire_time_changed(hass, future)
await hass.async_block_till_done()
@ -85,27 +92,12 @@ async def test_restoring_clients(hass: HomeAssistant) -> None:
registry.async_get_or_create(
"device_tracker",
DOMAIN,
DEFAULT_UNIQUE_ID,
DEFAULT_UNIQUEID,
suggested_object_id="ruckus_test_device",
config_entry=entry,
)
with patch(
"homeassistant.components.ruckus_unleashed.Ruckus.connect",
return_value=None,
), patch(
"homeassistant.components.ruckus_unleashed.Ruckus.mesh_name",
return_value=DEFAULT_TITLE,
), patch(
"homeassistant.components.ruckus_unleashed.Ruckus.system_info",
return_value=DEFAULT_SYSTEM_INFO,
), patch(
"homeassistant.components.ruckus_unleashed.Ruckus.ap_info",
return_value=DEFAULT_AP_INFO,
), patch(
"homeassistant.components.ruckus_unleashed.RuckusUnleashedDataUpdateCoordinator._fetch_clients",
return_value={},
):
with RuckusAjaxApiPatchContext(active_clients={}):
entry.add_to_hass(hass)
await hass.config_entries.async_setup(entry.entry_id)
await hass.async_block_till_done()

View File

@ -1,18 +1,16 @@
"""Test the Ruckus Unleashed config flow."""
from unittest.mock import patch
from unittest.mock import AsyncMock
from pyruckus.exceptions import AuthenticationError
from aioruckus.const import ERROR_CONNECT_TIMEOUT, ERROR_LOGIN_INCORRECT
from aioruckus.exceptions import AuthenticationError
from homeassistant.components.ruckus_unleashed import (
API_AP,
API_DEVICE_NAME,
API_ID,
API_MAC,
API_MODEL,
API_SYSTEM_OVERVIEW,
API_VERSION,
DOMAIN,
MANUFACTURER,
from homeassistant.components.ruckus_unleashed import DOMAIN, MANUFACTURER
from homeassistant.components.ruckus_unleashed.const import (
API_AP_DEVNAME,
API_AP_MAC,
API_AP_MODEL,
API_SYS_SYSINFO,
API_SYS_SYSINFO_VERSION,
)
from homeassistant.config_entries import ConfigEntryState
from homeassistant.core import HomeAssistant
@ -22,7 +20,7 @@ from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC
from . import (
DEFAULT_AP_INFO,
DEFAULT_SYSTEM_INFO,
DEFAULT_TITLE,
RuckusAjaxApiPatchContext,
init_integration,
mock_config_entry,
)
@ -31,9 +29,8 @@ from . import (
async def test_setup_entry_login_error(hass: HomeAssistant) -> None:
"""Test entry setup failed due to login error."""
entry = mock_config_entry()
with patch(
"homeassistant.components.ruckus_unleashed.Ruckus.connect",
side_effect=AuthenticationError,
with RuckusAjaxApiPatchContext(
login_mock=AsyncMock(side_effect=AuthenticationError(ERROR_LOGIN_INCORRECT))
):
entry.add_to_hass(hass)
result = await hass.config_entries.async_setup(entry.entry_id)
@ -45,9 +42,8 @@ async def test_setup_entry_login_error(hass: HomeAssistant) -> None:
async def test_setup_entry_connection_error(hass: HomeAssistant) -> None:
"""Test entry setup failed due to connection error."""
entry = mock_config_entry()
with patch(
"homeassistant.components.ruckus_unleashed.Ruckus.connect",
side_effect=ConnectionError,
with RuckusAjaxApiPatchContext(
login_mock=AsyncMock(side_effect=ConnectionError(ERROR_CONNECT_TIMEOUT))
):
entry.add_to_hass(hass)
await hass.config_entries.async_setup(entry.entry_id)
@ -60,19 +56,22 @@ async def test_router_device_setup(hass: HomeAssistant) -> None:
"""Test a router device is created."""
await init_integration(hass)
device_info = DEFAULT_AP_INFO[API_AP][API_ID]["1"]
device_info = DEFAULT_AP_INFO[0]
device_registry = dr.async_get(hass)
device = device_registry.async_get_device(
identifiers={(CONNECTION_NETWORK_MAC, device_info[API_MAC])},
connections={(CONNECTION_NETWORK_MAC, device_info[API_MAC])},
identifiers={(CONNECTION_NETWORK_MAC, device_info[API_AP_MAC])},
connections={(CONNECTION_NETWORK_MAC, device_info[API_AP_MAC])},
)
assert device
assert device.manufacturer == MANUFACTURER
assert device.model == device_info[API_MODEL]
assert device.name == device_info[API_DEVICE_NAME]
assert device.sw_version == DEFAULT_SYSTEM_INFO[API_SYSTEM_OVERVIEW][API_VERSION]
assert device.model == device_info[API_AP_MODEL]
assert device.name == device_info[API_AP_DEVNAME]
assert (
device.sw_version
== DEFAULT_SYSTEM_INFO[API_SYS_SYSINFO][API_SYS_SYSINFO_VERSION]
)
assert device.via_device_id is None
@ -83,31 +82,9 @@ async def test_unload_entry(hass: HomeAssistant) -> None:
assert len(hass.config_entries.async_entries(DOMAIN)) == 1
assert entry.state is ConfigEntryState.LOADED
assert await hass.config_entries.async_unload(entry.entry_id)
await hass.async_block_till_done()
with RuckusAjaxApiPatchContext():
assert await hass.config_entries.async_unload(entry.entry_id)
await hass.async_block_till_done()
assert entry.state is ConfigEntryState.NOT_LOADED
assert not hass.data.get(DOMAIN)
async def test_config_not_ready_during_setup(hass: HomeAssistant) -> None:
"""Test we throw a ConfigNotReady if Coordinator update fails."""
entry = mock_config_entry()
with patch(
"homeassistant.components.ruckus_unleashed.Ruckus.connect",
return_value=None,
), patch(
"homeassistant.components.ruckus_unleashed.Ruckus.mesh_name",
return_value=DEFAULT_TITLE,
), patch(
"homeassistant.components.ruckus_unleashed.Ruckus.system_info",
return_value=DEFAULT_SYSTEM_INFO,
), patch(
"homeassistant.components.ruckus_unleashed.RuckusUnleashedDataUpdateCoordinator._async_update_data",
side_effect=ConnectionError,
):
entry.add_to_hass(hass)
await hass.config_entries.async_setup(entry.entry_id)
await hass.async_block_till_done()
assert entry.state is ConfigEntryState.SETUP_RETRY