Ruckus Unleashed Requested Changes (#41824)

This commit is contained in:
Gabe Cook 2020-10-15 05:49:43 -05:00 committed by GitHub
parent a2574a4ed5
commit ffb4a9112b
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
12 changed files with 351 additions and 79 deletions

View file

@ -2,18 +2,30 @@
import asyncio import asyncio
from pyruckus import Ruckus from pyruckus import Ruckus
import voluptuous as vol
from homeassistant.config_entries import ConfigEntry from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.exceptions import ConfigEntryNotReady
from homeassistant.helpers import device_registry
from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC
from .const import COORDINATOR, DOMAIN, PLATFORMS, UNDO_UPDATE_LISTENERS from .const import (
API_AP,
API_DEVICE_NAME,
API_ID,
API_MAC,
API_MODEL,
API_SYSTEM_OVERVIEW,
API_VERSION,
COORDINATOR,
DOMAIN,
MANUFACTURER,
PLATFORMS,
UNDO_UPDATE_LISTENERS,
)
from .coordinator import RuckusUnleashedDataUpdateCoordinator from .coordinator import RuckusUnleashedDataUpdateCoordinator
CONFIG_SCHEMA = vol.Schema({DOMAIN: vol.Schema({})}, extra=vol.ALLOW_EXTRA)
async def async_setup(hass: HomeAssistant, entry: ConfigEntry) -> bool: async def async_setup(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Set up the Ruckus Unleashed component.""" """Set up the Ruckus Unleashed component."""
@ -39,6 +51,21 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
if not coordinator.last_update_success: if not coordinator.last_update_success:
raise ConfigEntryNotReady raise ConfigEntryNotReady
system_info = await hass.async_add_executor_job(ruckus.system_info)
registry = await device_registry.async_get_registry(hass)
ap_info = await hass.async_add_executor_job(ruckus.ap_info)
for device in ap_info[API_AP][API_ID].values():
registry.async_get_or_create(
config_entry_id=entry.entry_id,
connections={(CONNECTION_NETWORK_MAC, device[API_MAC])},
identifiers={(CONNECTION_NETWORK_MAC, device[API_MAC])},
manufacturer=MANUFACTURER,
name=device[API_DEVICE_NAME],
model=device[API_MODEL],
sw_version=system_info[API_SYSTEM_OVERVIEW][API_VERSION],
)
hass.data[DOMAIN][entry.entry_id] = { hass.data[DOMAIN][entry.entry_id] = {
COORDINATOR: coordinator, COORDINATOR: coordinator,
UNDO_UPDATE_LISTENERS: [], UNDO_UPDATE_LISTENERS: [],

View file

@ -1,4 +1,6 @@
"""Config flow for Ruckus Unleashed integration.""" """Config flow for Ruckus Unleashed integration."""
import logging
from pyruckus import Ruckus from pyruckus import Ruckus
from pyruckus.exceptions import AuthenticationError from pyruckus.exceptions import AuthenticationError
import voluptuous as vol import voluptuous as vol
@ -6,21 +8,25 @@ import voluptuous as vol
from homeassistant import config_entries, core, exceptions from homeassistant import config_entries, core, exceptions
from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME
from .const import _LOGGER, DOMAIN # pylint:disable=unused-import from .const import ( # pylint:disable=unused-import
API_SERIAL,
API_SYSTEM_OVERVIEW,
DOMAIN,
)
_LOGGER = logging.getLogger(__package__)
DATA_SCHEMA = vol.Schema({"host": str, "username": str, "password": str}) DATA_SCHEMA = vol.Schema({"host": str, "username": str, "password": str})
async def validate_input(hass: core.HomeAssistant, data): def validate_input(hass: core.HomeAssistant, data):
"""Validate the user input allows us to connect. """Validate the user input allows us to connect.
Data has the keys from DATA_SCHEMA with values provided by the user. Data has the keys from DATA_SCHEMA with values provided by the user.
""" """
try: try:
ruckus = await hass.async_add_executor_job( ruckus = Ruckus(data[CONF_HOST], data[CONF_USERNAME], data[CONF_PASSWORD])
Ruckus, data[CONF_HOST], data[CONF_USERNAME], data[CONF_PASSWORD]
)
except AuthenticationError as error: except AuthenticationError as error:
raise InvalidAuth from error raise InvalidAuth from error
except ConnectionError as error: except ConnectionError as error:
@ -28,7 +34,16 @@ async def validate_input(hass: core.HomeAssistant, data):
mesh_name = ruckus.mesh_name() mesh_name = ruckus.mesh_name()
return {"title": mesh_name} system_info = 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,
}
class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
@ -42,7 +57,9 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
errors = {} errors = {}
if user_input is not None: if user_input is not None:
try: try:
info = await validate_input(self.hass, user_input) info = await self.hass.async_add_executor_job(
validate_input, self.hass, user_input
)
except CannotConnect: except CannotConnect:
errors["base"] = "cannot_connect" errors["base"] = "cannot_connect"
except InvalidAuth: except InvalidAuth:
@ -51,7 +68,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
_LOGGER.exception("Unexpected exception") _LOGGER.exception("Unexpected exception")
errors["base"] = "unknown" errors["base"] = "unknown"
else: else:
await self.async_set_unique_id(user_input[CONF_HOST]) await self.async_set_unique_id(info["serial"])
self._abort_if_unique_id_configured() self._abort_if_unique_id_configured()
return self.async_create_entry(title=info["title"], data=user_input) return self.async_create_entry(title=info["title"], data=user_input)

View file

@ -1,13 +1,23 @@
"""Constants for the Ruckus Unleashed integration.""" """Constants for the Ruckus Unleashed integration."""
import logging
DOMAIN = "ruckus_unleashed" DOMAIN = "ruckus_unleashed"
PLATFORMS = ["device_tracker"] PLATFORMS = ["device_tracker"]
SCAN_INTERVAL = 180 SCAN_INTERVAL = 180
_LOGGER = logging.getLogger(__name__) MANUFACTURER = "Ruckus"
COORDINATOR = "coordinator" COORDINATOR = "coordinator"
UNDO_UPDATE_LISTENERS = "undo_update_listeners" UNDO_UPDATE_LISTENERS = "undo_update_listeners"
CLIENTS = "clients" 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"

View file

@ -1,5 +1,6 @@
"""Ruckus Unleashed DataUpdateCoordinator.""" """Ruckus Unleashed DataUpdateCoordinator."""
from datetime import timedelta from datetime import timedelta
import logging
from pyruckus import Ruckus from pyruckus import Ruckus
from pyruckus.exceptions import AuthenticationError from pyruckus.exceptions import AuthenticationError
@ -7,7 +8,15 @@ from pyruckus.exceptions import AuthenticationError
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
from .const import _LOGGER, CLIENTS, DOMAIN, SCAN_INTERVAL from .const import (
API_CLIENTS,
API_CURRENT_ACTIVE_CLIENTS,
API_MAC,
DOMAIN,
SCAN_INTERVAL,
)
_LOGGER = logging.getLogger(__package__)
class RuckusUnleashedDataUpdateCoordinator(DataUpdateCoordinator): class RuckusUnleashedDataUpdateCoordinator(DataUpdateCoordinator):
@ -26,11 +35,16 @@ class RuckusUnleashedDataUpdateCoordinator(DataUpdateCoordinator):
update_interval=update_interval, update_interval=update_interval,
) )
async def _fetch_clients(self) -> dict:
"""Fetch clients from the API and format them."""
clients = await self.hass.async_add_executor_job(
self.ruckus.current_active_clients
)
return {e[API_MAC]: e for e in clients[API_CURRENT_ACTIVE_CLIENTS][API_CLIENTS]}
async def _async_update_data(self) -> dict: async def _async_update_data(self) -> dict:
"""Fetch Ruckus Unleashed data.""" """Fetch Ruckus Unleashed data."""
try: try:
return { return {API_CLIENTS: await self._fetch_clients()}
CLIENTS: await self.hass.async_add_executor_job(self.ruckus.clients)
}
except (AuthenticationError, ConnectionError) as error: except (AuthenticationError, ConnectionError) as error:
raise UpdateFailed(error) from error raise UpdateFailed(error) from error

View file

@ -1,18 +1,24 @@
"""Support for Ruckus Unleashed devices.""" """Support for Ruckus Unleashed devices."""
from homeassistant.components.device_tracker import ( from typing import Optional
ATTR_MAC,
ATTR_SOURCE_TYPE, from homeassistant.components.device_tracker import SOURCE_TYPE_ROUTER
SOURCE_TYPE_ROUTER,
)
from homeassistant.components.device_tracker.config_entry import ScannerEntity from homeassistant.components.device_tracker.config_entry import ScannerEntity
from homeassistant.config_entries import ConfigEntry from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_NAME
from homeassistant.core import callback from homeassistant.core import callback
from homeassistant.helpers import entity_registry from homeassistant.helpers import entity_registry
from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC
from homeassistant.helpers.typing import HomeAssistantType from homeassistant.helpers.typing import HomeAssistantType
from homeassistant.helpers.update_coordinator import CoordinatorEntity from homeassistant.helpers.update_coordinator import CoordinatorEntity
from .const import CLIENTS, COORDINATOR, DOMAIN, UNDO_UPDATE_LISTENERS from .const import (
API_ACCESS_POINT,
API_CLIENTS,
API_NAME,
COORDINATOR,
DOMAIN,
MANUFACTURER,
UNDO_UPDATE_LISTENERS,
)
async def async_setup_entry( async def async_setup_entry(
@ -43,16 +49,16 @@ def add_new_entities(coordinator, async_add_entities, tracked):
"""Add new tracker entities from the router.""" """Add new tracker entities from the router."""
new_tracked = [] new_tracked = []
for mac in coordinator.data[CLIENTS].keys(): for mac in coordinator.data[API_CLIENTS]:
if mac in tracked: if mac in tracked:
continue continue
device = coordinator.data[CLIENTS][mac] device = coordinator.data[API_CLIENTS][mac]
new_tracked.append(RuckusUnleashedDevice(coordinator, mac, device[CONF_NAME])) new_tracked.append(RuckusUnleashedDevice(coordinator, mac, device[API_NAME]))
tracked.add(mac) tracked.add(mac)
if new_tracked: if new_tracked:
async_add_entities(new_tracked, True) async_add_entities(new_tracked)
@callback @callback
@ -62,7 +68,7 @@ def restore_entities(registry, coordinator, entry, async_add_entities, tracked):
for entity in registry.entities.values(): for entity in registry.entities.values():
if entity.config_entry_id == entry.entry_id and entity.platform == DOMAIN: if entity.config_entry_id == entry.entry_id and entity.platform == DOMAIN:
if entity.unique_id not in coordinator.data[CLIENTS]: if entity.unique_id not in coordinator.data[API_CLIENTS]:
missing.append( missing.append(
RuckusUnleashedDevice( RuckusUnleashedDevice(
coordinator, entity.unique_id, entity.original_name coordinator, entity.unique_id, entity.original_name
@ -71,7 +77,7 @@ def restore_entities(registry, coordinator, entry, async_add_entities, tracked):
tracked.add(entity.unique_id) tracked.add(entity.unique_id)
if missing: if missing:
async_add_entities(missing, True) async_add_entities(missing)
class RuckusUnleashedDevice(CoordinatorEntity, ScannerEntity): class RuckusUnleashedDevice(CoordinatorEntity, ScannerEntity):
@ -93,15 +99,15 @@ class RuckusUnleashedDevice(CoordinatorEntity, ScannerEntity):
"""Return the name.""" """Return the name."""
if self.is_connected: if self.is_connected:
return ( return (
self.coordinator.data[CLIENTS][self._mac][CONF_NAME] self.coordinator.data[API_CLIENTS][self._mac][API_NAME]
or f"Ruckus {self._mac}" or f"{MANUFACTURER} {self._mac}"
) )
return self._name return self._name
@property @property
def is_connected(self) -> bool: def is_connected(self) -> bool:
"""Return true if the device is connected to the network.""" """Return true if the device is connected to the network."""
return self._mac in self.coordinator.data[CLIENTS] return self._mac in self.coordinator.data[API_CLIENTS]
@property @property
def source_type(self) -> str: def source_type(self) -> str:
@ -109,9 +115,15 @@ class RuckusUnleashedDevice(CoordinatorEntity, ScannerEntity):
return SOURCE_TYPE_ROUTER return SOURCE_TYPE_ROUTER
@property @property
def state_attributes(self) -> dict: def device_info(self) -> Optional[dict]:
"""Return the state attributes.""" """Return the device information."""
return { if self.is_connected:
ATTR_SOURCE_TYPE: self.source_type, return {
ATTR_MAC: self._mac, "name": self.name,
} "connections": {(CONNECTION_NETWORK_MAC, self._mac)},
"via_device": (
CONNECTION_NETWORK_MAC,
self.coordinator.data[API_CLIENTS][self._mac][API_ACCESS_POINT],
),
}
return None

View file

@ -4,12 +4,8 @@
"config_flow": true, "config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/ruckus_unleashed", "documentation": "https://www.home-assistant.io/integrations/ruckus_unleashed",
"requirements": [ "requirements": [
"pyruckus==0.7" "pyruckus==0.12"
], ],
"ssdp": [],
"zeroconf": [],
"homekit": {},
"dependencies": [],
"codeowners": [ "codeowners": [
"@gabe565" "@gabe565"
] ]

View file

@ -1633,7 +1633,7 @@ pyrepetier==3.0.5
pyrisco==0.3.1 pyrisco==0.3.1
# homeassistant.components.ruckus_unleashed # homeassistant.components.ruckus_unleashed
pyruckus==0.7 pyruckus==0.12
# homeassistant.components.sabnzbd # homeassistant.components.sabnzbd
pysabnzbd==1.1.0 pysabnzbd==1.1.0

View file

@ -801,7 +801,7 @@ pyqwikswitch==0.93
pyrisco==0.3.1 pyrisco==0.3.1
# homeassistant.components.ruckus_unleashed # homeassistant.components.ruckus_unleashed
pyruckus==0.7 pyruckus==0.12
# homeassistant.components.acer_projector # homeassistant.components.acer_projector
# homeassistant.components.zha # homeassistant.components.zha

View file

@ -1,18 +1,42 @@
"""Tests for the Ruckus Unleashed integration.""" """Tests for the Ruckus Unleashed integration."""
from homeassistant.components.ruckus_unleashed import DOMAIN from homeassistant.components.ruckus_unleashed import DOMAIN
from homeassistant.const import ( from homeassistant.components.ruckus_unleashed.const import (
CONF_HOST, API_ACCESS_POINT,
CONF_IP_ADDRESS, API_AP,
CONF_MAC, API_DEVICE_NAME,
CONF_NAME, API_ID,
CONF_PASSWORD, API_IP,
CONF_USERNAME, API_MAC,
API_MODEL,
API_NAME,
API_SERIAL,
API_SYSTEM_OVERVIEW,
API_VERSION,
) )
from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME
from tests.async_mock import patch from tests.async_mock import patch
from tests.common import MockConfigEntry from tests.common import MockConfigEntry
DEFAULT_TITLE = "Ruckus Mesh" DEFAULT_TITLE = "Ruckus Mesh"
DEFAULT_UNIQUE_ID = "123456789012"
DEFAULT_SYSTEM_INFO = {
API_SYSTEM_OVERVIEW: {
API_SERIAL: DEFAULT_UNIQUE_ID,
API_VERSION: "v1.0.0",
}
}
DEFAULT_AP_INFO = {
API_AP: {
API_ID: {
"1": {
API_MAC: "00:11:22:33:44:55",
API_DEVICE_NAME: "Test Device",
API_MODEL: "r510",
}
}
}
}
CONFIG = { CONFIG = {
CONF_HOST: "1.1.1.1", CONF_HOST: "1.1.1.1",
@ -22,9 +46,10 @@ CONFIG = {
TEST_CLIENT_ENTITY_ID = "device_tracker.ruckus_test_device" TEST_CLIENT_ENTITY_ID = "device_tracker.ruckus_test_device"
TEST_CLIENT = { TEST_CLIENT = {
CONF_IP_ADDRESS: "1.1.1.2", API_IP: "1.1.1.2",
CONF_MAC: "AA:BB:CC:DD:EE:FF", API_MAC: "AA:BB:CC:DD:EE:FF",
CONF_NAME: "Ruckus Test Device", API_NAME: "Ruckus Test Device",
API_ACCESS_POINT: "00:11:22:33:44:55",
} }
@ -33,7 +58,7 @@ def mock_config_entry() -> MockConfigEntry:
return MockConfigEntry( return MockConfigEntry(
domain=DOMAIN, domain=DOMAIN,
title=DEFAULT_TITLE, title=DEFAULT_TITLE,
unique_id="1.1.1.1", unique_id=DEFAULT_UNIQUE_ID,
data=CONFIG, data=CONFIG,
options=None, options=None,
) )
@ -49,9 +74,15 @@ async def init_integration(hass) -> MockConfigEntry:
"homeassistant.components.ruckus_unleashed.Ruckus.mesh_name", "homeassistant.components.ruckus_unleashed.Ruckus.mesh_name",
return_value=DEFAULT_TITLE, return_value=DEFAULT_TITLE,
), patch( ), patch(
"homeassistant.components.ruckus_unleashed.Ruckus.clients", "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={ return_value={
TEST_CLIENT[CONF_MAC]: TEST_CLIENT, TEST_CLIENT[API_MAC]: TEST_CLIENT,
}, },
): ):
entry.add_to_hass(hass) entry.add_to_hass(hass)

View file

@ -1,16 +1,19 @@
"""Test the Ruckus Unleashed config flow.""" """Test the Ruckus Unleashed config flow."""
from datetime import timedelta
from pyruckus.exceptions import AuthenticationError from pyruckus.exceptions import AuthenticationError
from homeassistant import config_entries, setup from homeassistant import config_entries
from homeassistant.components.ruckus_unleashed.const import DOMAIN from homeassistant.components.ruckus_unleashed.const import DOMAIN
from homeassistant.util import utcnow
from tests.async_mock import patch from tests.async_mock import patch
from tests.components.ruckus_unleashed import CONFIG, DEFAULT_TITLE from tests.common import async_fire_time_changed
from tests.components.ruckus_unleashed import CONFIG, DEFAULT_SYSTEM_INFO, DEFAULT_TITLE
async def test_form(hass): async def test_form(hass):
"""Test we get the form.""" """Test we get the form."""
await setup.async_setup_component(hass, "persistent_notification", {})
result = await hass.config_entries.flow.async_init( result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": config_entries.SOURCE_USER} DOMAIN, context={"source": config_entries.SOURCE_USER}
) )
@ -23,6 +26,9 @@ async def test_form(hass):
), patch( ), patch(
"homeassistant.components.ruckus_unleashed.Ruckus.mesh_name", "homeassistant.components.ruckus_unleashed.Ruckus.mesh_name",
return_value=DEFAULT_TITLE, return_value=DEFAULT_TITLE,
), patch(
"homeassistant.components.ruckus_unleashed.Ruckus.system_info",
return_value=DEFAULT_SYSTEM_INFO,
), patch( ), patch(
"homeassistant.components.ruckus_unleashed.async_setup", return_value=True "homeassistant.components.ruckus_unleashed.async_setup", return_value=True
) as mock_setup, patch( ) as mock_setup, patch(
@ -97,3 +103,70 @@ async def test_form_unknown_error(hass):
assert result2["type"] == "form" assert result2["type"] == "form"
assert result2["errors"] == {"base": "unknown"} assert result2["errors"] == {"base": "unknown"}
async def test_form_cannot_connect_unknown_serial(hass):
"""Test we handle cannot connect error on invalid serial number."""
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": config_entries.SOURCE_USER}
)
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={},
):
result2 = await hass.config_entries.flow.async_configure(
result["flow_id"],
CONFIG,
)
assert result2["type"] == "form"
assert result2["errors"] == {"base": "cannot_connect"}
async def test_form_duplicate_error(hass):
"""Test we handle duplicate error."""
result = await hass.config_entries.flow.async_init(
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,
):
await hass.config_entries.flow.async_configure(
result["flow_id"],
CONFIG,
)
future = utcnow() + timedelta(minutes=60)
async_fire_time_changed(hass, future)
await hass.async_block_till_done()
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": config_entries.SOURCE_USER}
)
assert result["type"] == "form"
assert result["errors"] == {}
result2 = await hass.config_entries.flow.async_configure(
result["flow_id"],
CONFIG,
)
assert result2["type"] == "abort"
assert result2["reason"] == "already_configured"

View file

@ -1,15 +1,20 @@
"""The sensor tests for the Ruckus Unleashed platform.""" """The sensor tests for the Ruckus Unleashed platform."""
from datetime import timedelta from datetime import timedelta
from homeassistant.components.ruckus_unleashed import DOMAIN from homeassistant.components.ruckus_unleashed import API_MAC, DOMAIN
from homeassistant.const import CONF_MAC, STATE_HOME, STATE_NOT_HOME, STATE_UNAVAILABLE from homeassistant.components.ruckus_unleashed.const import API_AP, API_ID, API_NAME
from homeassistant.const import STATE_HOME, STATE_NOT_HOME, STATE_UNAVAILABLE
from homeassistant.helpers import entity_registry from homeassistant.helpers import entity_registry
from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC
from homeassistant.util import utcnow from homeassistant.util import utcnow
from tests.async_mock import patch from tests.async_mock import patch
from tests.common import async_fire_time_changed from tests.common import async_fire_time_changed
from tests.components.ruckus_unleashed import ( from tests.components.ruckus_unleashed import (
DEFAULT_AP_INFO,
DEFAULT_SYSTEM_INFO,
DEFAULT_TITLE, DEFAULT_TITLE,
DEFAULT_UNIQUE_ID,
TEST_CLIENT, TEST_CLIENT,
TEST_CLIENT_ENTITY_ID, TEST_CLIENT_ENTITY_ID,
init_integration, init_integration,
@ -23,17 +28,17 @@ async def test_client_connected(hass):
future = utcnow() + timedelta(minutes=60) future = utcnow() + timedelta(minutes=60)
with patch( with patch(
"homeassistant.components.ruckus_unleashed.Ruckus.clients", "homeassistant.components.ruckus_unleashed.RuckusUnleashedDataUpdateCoordinator._fetch_clients",
return_value={ return_value={
TEST_CLIENT[CONF_MAC]: TEST_CLIENT, TEST_CLIENT[API_MAC]: TEST_CLIENT,
}, },
): ):
async_fire_time_changed(hass, future) async_fire_time_changed(hass, future)
await hass.async_block_till_done() await hass.async_block_till_done()
await hass.helpers.entity_component.async_update_entity(TEST_CLIENT_ENTITY_ID) await hass.helpers.entity_component.async_update_entity(TEST_CLIENT_ENTITY_ID)
test_client = hass.states.get(TEST_CLIENT_ENTITY_ID)
assert test_client.state == STATE_HOME test_client = hass.states.get(TEST_CLIENT_ENTITY_ID)
assert test_client.state == STATE_HOME
async def test_client_disconnected(hass): async def test_client_disconnected(hass):
@ -42,7 +47,7 @@ async def test_client_disconnected(hass):
future = utcnow() + timedelta(minutes=60) future = utcnow() + timedelta(minutes=60)
with patch( with patch(
"homeassistant.components.ruckus_unleashed.Ruckus.clients", "homeassistant.components.ruckus_unleashed.RuckusUnleashedDataUpdateCoordinator._fetch_clients",
return_value={}, return_value={},
): ):
async_fire_time_changed(hass, future) async_fire_time_changed(hass, future)
@ -59,7 +64,7 @@ async def test_clients_update_failed(hass):
future = utcnow() + timedelta(minutes=60) future = utcnow() + timedelta(minutes=60)
with patch( with patch(
"homeassistant.components.ruckus_unleashed.Ruckus.clients", "homeassistant.components.ruckus_unleashed.RuckusUnleashedDataUpdateCoordinator._fetch_clients",
side_effect=ConnectionError, side_effect=ConnectionError,
): ):
async_fire_time_changed(hass, future) async_fire_time_changed(hass, future)
@ -79,7 +84,7 @@ async def test_restoring_clients(hass):
registry.async_get_or_create( registry.async_get_or_create(
"device_tracker", "device_tracker",
DOMAIN, DOMAIN,
TEST_CLIENT[CONF_MAC], DEFAULT_UNIQUE_ID,
suggested_object_id="ruckus_test_device", suggested_object_id="ruckus_test_device",
config_entry=entry, config_entry=entry,
) )
@ -91,7 +96,13 @@ async def test_restoring_clients(hass):
"homeassistant.components.ruckus_unleashed.Ruckus.mesh_name", "homeassistant.components.ruckus_unleashed.Ruckus.mesh_name",
return_value=DEFAULT_TITLE, return_value=DEFAULT_TITLE,
), patch( ), patch(
"homeassistant.components.ruckus_unleashed.Ruckus.clients", "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={}, return_value={},
): ):
entry.add_to_hass(hass) entry.add_to_hass(hass)
@ -101,3 +112,24 @@ async def test_restoring_clients(hass):
device = hass.states.get(TEST_CLIENT_ENTITY_ID) device = hass.states.get(TEST_CLIENT_ENTITY_ID)
assert device is not None assert device is not None
assert device.state == STATE_NOT_HOME assert device.state == STATE_NOT_HOME
async def test_client_device_setup(hass):
"""Test a client device is created."""
await init_integration(hass)
router_info = DEFAULT_AP_INFO[API_AP][API_ID]["1"]
device_registry = await hass.helpers.device_registry.async_get_registry()
client_device = device_registry.async_get_device(
identifiers={},
connections={(CONNECTION_NETWORK_MAC, TEST_CLIENT[API_MAC])},
)
router_device = device_registry.async_get_device(
identifiers={(CONNECTION_NETWORK_MAC, router_info[API_MAC])},
connections={(CONNECTION_NETWORK_MAC, router_info[API_MAC])},
)
assert client_device
assert client_device.name == TEST_CLIENT[API_NAME]
assert client_device.via_device_id == router_device.id

View file

@ -1,15 +1,32 @@
"""Test the Ruckus Unleashed config flow.""" """Test the Ruckus Unleashed config flow."""
from pyruckus.exceptions import AuthenticationError from pyruckus.exceptions import AuthenticationError
from homeassistant.components.ruckus_unleashed import DOMAIN 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.config_entries import ( from homeassistant.config_entries import (
ENTRY_STATE_LOADED, ENTRY_STATE_LOADED,
ENTRY_STATE_NOT_LOADED, ENTRY_STATE_NOT_LOADED,
ENTRY_STATE_SETUP_RETRY, ENTRY_STATE_SETUP_RETRY,
) )
from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC
from tests.async_mock import patch from tests.async_mock import patch
from tests.components.ruckus_unleashed import init_integration, mock_config_entry from tests.components.ruckus_unleashed import (
DEFAULT_AP_INFO,
DEFAULT_SYSTEM_INFO,
DEFAULT_TITLE,
init_integration,
mock_config_entry,
)
async def test_setup_entry_login_error(hass): async def test_setup_entry_login_error(hass):
@ -40,6 +57,26 @@ async def test_setup_entry_connection_error(hass):
assert entry.state == ENTRY_STATE_SETUP_RETRY assert entry.state == ENTRY_STATE_SETUP_RETRY
async def test_router_device_setup(hass):
"""Test a router device is created."""
await init_integration(hass)
device_info = DEFAULT_AP_INFO[API_AP][API_ID]["1"]
device_registry = await hass.helpers.device_registry.async_get_registry()
device = device_registry.async_get_device(
identifiers={(CONNECTION_NETWORK_MAC, device_info[API_MAC])},
connections={(CONNECTION_NETWORK_MAC, device_info[API_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.via_device_id is None
async def test_unload_entry(hass): async def test_unload_entry(hass):
"""Test successful unload of entry.""" """Test successful unload of entry."""
entry = await init_integration(hass) entry = await init_integration(hass)
@ -52,3 +89,26 @@ async def test_unload_entry(hass):
assert entry.state == ENTRY_STATE_NOT_LOADED assert entry.state == ENTRY_STATE_NOT_LOADED
assert not hass.data.get(DOMAIN) assert not hass.data.get(DOMAIN)
async def test_config_not_ready_during_setup(hass):
"""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 == ENTRY_STATE_SETUP_RETRY