diff --git a/homeassistant/components/ruckus_unleashed/__init__.py b/homeassistant/components/ruckus_unleashed/__init__.py index 5547e6ebb70..ed38f5a2a3a 100644 --- a/homeassistant/components/ruckus_unleashed/__init__.py +++ b/homeassistant/components/ruckus_unleashed/__init__.py @@ -2,18 +2,30 @@ import asyncio from pyruckus import Ruckus -import voluptuous as vol 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.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 -CONFIG_SCHEMA = vol.Schema({DOMAIN: vol.Schema({})}, extra=vol.ALLOW_EXTRA) - async def async_setup(hass: HomeAssistant, entry: ConfigEntry) -> bool: """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: 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] = { COORDINATOR: coordinator, UNDO_UPDATE_LISTENERS: [], diff --git a/homeassistant/components/ruckus_unleashed/config_flow.py b/homeassistant/components/ruckus_unleashed/config_flow.py index 34fca5b1c4a..098ffba708c 100644 --- a/homeassistant/components/ruckus_unleashed/config_flow.py +++ b/homeassistant/components/ruckus_unleashed/config_flow.py @@ -1,4 +1,6 @@ """Config flow for Ruckus Unleashed integration.""" +import logging + from pyruckus import Ruckus from pyruckus.exceptions import AuthenticationError import voluptuous as vol @@ -6,21 +8,25 @@ import voluptuous as vol from homeassistant import config_entries, core, exceptions 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}) -async def validate_input(hass: core.HomeAssistant, data): +def validate_input(hass: core.HomeAssistant, data): """Validate the user input allows us to connect. Data has the keys from DATA_SCHEMA with values provided by the user. """ try: - ruckus = await hass.async_add_executor_job( - Ruckus, data[CONF_HOST], data[CONF_USERNAME], data[CONF_PASSWORD] - ) + ruckus = Ruckus(data[CONF_HOST], data[CONF_USERNAME], data[CONF_PASSWORD]) except AuthenticationError as error: raise InvalidAuth from error except ConnectionError as error: @@ -28,7 +34,16 @@ async def validate_input(hass: core.HomeAssistant, data): 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): @@ -42,7 +57,9 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): errors = {} if user_input is not None: 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: errors["base"] = "cannot_connect" except InvalidAuth: @@ -51,7 +68,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): _LOGGER.exception("Unexpected exception") errors["base"] = "unknown" 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() return self.async_create_entry(title=info["title"], data=user_input) diff --git a/homeassistant/components/ruckus_unleashed/const.py b/homeassistant/components/ruckus_unleashed/const.py index 04ea864a3c3..77e1029143e 100644 --- a/homeassistant/components/ruckus_unleashed/const.py +++ b/homeassistant/components/ruckus_unleashed/const.py @@ -1,13 +1,23 @@ """Constants for the Ruckus Unleashed integration.""" -import logging - DOMAIN = "ruckus_unleashed" PLATFORMS = ["device_tracker"] SCAN_INTERVAL = 180 -_LOGGER = logging.getLogger(__name__) +MANUFACTURER = "Ruckus" COORDINATOR = "coordinator" 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" diff --git a/homeassistant/components/ruckus_unleashed/coordinator.py b/homeassistant/components/ruckus_unleashed/coordinator.py index b57243c5f17..1e145a19c74 100644 --- a/homeassistant/components/ruckus_unleashed/coordinator.py +++ b/homeassistant/components/ruckus_unleashed/coordinator.py @@ -1,5 +1,6 @@ """Ruckus Unleashed DataUpdateCoordinator.""" from datetime import timedelta +import logging from pyruckus import Ruckus from pyruckus.exceptions import AuthenticationError @@ -7,7 +8,15 @@ from pyruckus.exceptions import AuthenticationError from homeassistant.core import HomeAssistant 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): @@ -26,11 +35,16 @@ class RuckusUnleashedDataUpdateCoordinator(DataUpdateCoordinator): 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: """Fetch Ruckus Unleashed data.""" try: - return { - CLIENTS: await self.hass.async_add_executor_job(self.ruckus.clients) - } + return {API_CLIENTS: await self._fetch_clients()} except (AuthenticationError, ConnectionError) as error: raise UpdateFailed(error) from error diff --git a/homeassistant/components/ruckus_unleashed/device_tracker.py b/homeassistant/components/ruckus_unleashed/device_tracker.py index b64aa960727..955e0581393 100644 --- a/homeassistant/components/ruckus_unleashed/device_tracker.py +++ b/homeassistant/components/ruckus_unleashed/device_tracker.py @@ -1,18 +1,24 @@ """Support for Ruckus Unleashed devices.""" -from homeassistant.components.device_tracker import ( - ATTR_MAC, - ATTR_SOURCE_TYPE, - SOURCE_TYPE_ROUTER, -) +from typing import Optional + +from homeassistant.components.device_tracker import SOURCE_TYPE_ROUTER from homeassistant.components.device_tracker.config_entry import ScannerEntity from homeassistant.config_entries import ConfigEntry -from homeassistant.const import CONF_NAME from homeassistant.core import callback from homeassistant.helpers import entity_registry +from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC from homeassistant.helpers.typing import HomeAssistantType 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( @@ -43,16 +49,16 @@ def add_new_entities(coordinator, async_add_entities, tracked): """Add new tracker entities from the router.""" new_tracked = [] - for mac in coordinator.data[CLIENTS].keys(): + for mac in coordinator.data[API_CLIENTS]: if mac in tracked: continue - device = coordinator.data[CLIENTS][mac] - new_tracked.append(RuckusUnleashedDevice(coordinator, mac, device[CONF_NAME])) + device = coordinator.data[API_CLIENTS][mac] + new_tracked.append(RuckusUnleashedDevice(coordinator, mac, device[API_NAME])) tracked.add(mac) if new_tracked: - async_add_entities(new_tracked, True) + async_add_entities(new_tracked) @callback @@ -62,7 +68,7 @@ def restore_entities(registry, coordinator, entry, async_add_entities, tracked): for entity in registry.entities.values(): 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( RuckusUnleashedDevice( 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) if missing: - async_add_entities(missing, True) + async_add_entities(missing) class RuckusUnleashedDevice(CoordinatorEntity, ScannerEntity): @@ -93,15 +99,15 @@ class RuckusUnleashedDevice(CoordinatorEntity, ScannerEntity): """Return the name.""" if self.is_connected: return ( - self.coordinator.data[CLIENTS][self._mac][CONF_NAME] - or f"Ruckus {self._mac}" + self.coordinator.data[API_CLIENTS][self._mac][API_NAME] + or f"{MANUFACTURER} {self._mac}" ) return self._name @property def is_connected(self) -> bool: """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 def source_type(self) -> str: @@ -109,9 +115,15 @@ class RuckusUnleashedDevice(CoordinatorEntity, ScannerEntity): return SOURCE_TYPE_ROUTER @property - def state_attributes(self) -> dict: - """Return the state attributes.""" - return { - ATTR_SOURCE_TYPE: self.source_type, - ATTR_MAC: self._mac, - } + def device_info(self) -> Optional[dict]: + """Return the device information.""" + if self.is_connected: + return { + "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 diff --git a/homeassistant/components/ruckus_unleashed/manifest.json b/homeassistant/components/ruckus_unleashed/manifest.json index c9b9b6fed62..b8bc14a108a 100644 --- a/homeassistant/components/ruckus_unleashed/manifest.json +++ b/homeassistant/components/ruckus_unleashed/manifest.json @@ -4,13 +4,9 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/ruckus_unleashed", "requirements": [ - "pyruckus==0.7" + "pyruckus==0.12" ], - "ssdp": [], - "zeroconf": [], - "homekit": {}, - "dependencies": [], "codeowners": [ "@gabe565" ] -} \ No newline at end of file +} diff --git a/requirements_all.txt b/requirements_all.txt index 78b57616e7d..15bbae1b0fe 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1633,7 +1633,7 @@ pyrepetier==3.0.5 pyrisco==0.3.1 # homeassistant.components.ruckus_unleashed -pyruckus==0.7 +pyruckus==0.12 # homeassistant.components.sabnzbd pysabnzbd==1.1.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 8f9b4fcebe8..07c872fb240 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -801,7 +801,7 @@ pyqwikswitch==0.93 pyrisco==0.3.1 # homeassistant.components.ruckus_unleashed -pyruckus==0.7 +pyruckus==0.12 # homeassistant.components.acer_projector # homeassistant.components.zha diff --git a/tests/components/ruckus_unleashed/__init__.py b/tests/components/ruckus_unleashed/__init__.py index 5329d253aff..7efbfc03457 100644 --- a/tests/components/ruckus_unleashed/__init__.py +++ b/tests/components/ruckus_unleashed/__init__.py @@ -1,18 +1,42 @@ """Tests for the Ruckus Unleashed integration.""" from homeassistant.components.ruckus_unleashed import DOMAIN -from homeassistant.const import ( - CONF_HOST, - CONF_IP_ADDRESS, - CONF_MAC, - CONF_NAME, - CONF_PASSWORD, - CONF_USERNAME, +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, ) +from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME from tests.async_mock import patch 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", + } +} +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 = { CONF_HOST: "1.1.1.1", @@ -22,9 +46,10 @@ CONFIG = { TEST_CLIENT_ENTITY_ID = "device_tracker.ruckus_test_device" TEST_CLIENT = { - CONF_IP_ADDRESS: "1.1.1.2", - CONF_MAC: "AA:BB:CC:DD:EE:FF", - CONF_NAME: "Ruckus Test Device", + 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", } @@ -33,7 +58,7 @@ def mock_config_entry() -> MockConfigEntry: return MockConfigEntry( domain=DOMAIN, title=DEFAULT_TITLE, - unique_id="1.1.1.1", + unique_id=DEFAULT_UNIQUE_ID, data=CONFIG, options=None, ) @@ -49,9 +74,15 @@ async def init_integration(hass) -> MockConfigEntry: "homeassistant.components.ruckus_unleashed.Ruckus.mesh_name", return_value=DEFAULT_TITLE, ), 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={ - TEST_CLIENT[CONF_MAC]: TEST_CLIENT, + TEST_CLIENT[API_MAC]: TEST_CLIENT, }, ): entry.add_to_hass(hass) diff --git a/tests/components/ruckus_unleashed/test_config_flow.py b/tests/components/ruckus_unleashed/test_config_flow.py index 3f6ce6119fc..8348ec4f2a8 100644 --- a/tests/components/ruckus_unleashed/test_config_flow.py +++ b/tests/components/ruckus_unleashed/test_config_flow.py @@ -1,16 +1,19 @@ """Test the Ruckus Unleashed config flow.""" +from datetime import timedelta + 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.util import utcnow 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): """Test we get the form.""" - await setup.async_setup_component(hass, "persistent_notification", {}) result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) @@ -23,6 +26,9 @@ async def test_form(hass): ), 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.async_setup", return_value=True ) as mock_setup, patch( @@ -97,3 +103,70 @@ async def test_form_unknown_error(hass): assert result2["type"] == "form" 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" diff --git a/tests/components/ruckus_unleashed/test_device_tracker.py b/tests/components/ruckus_unleashed/test_device_tracker.py index cd529ac6bad..38736b0117f 100644 --- a/tests/components/ruckus_unleashed/test_device_tracker.py +++ b/tests/components/ruckus_unleashed/test_device_tracker.py @@ -1,15 +1,20 @@ """The sensor tests for the Ruckus Unleashed platform.""" from datetime import timedelta -from homeassistant.components.ruckus_unleashed import DOMAIN -from homeassistant.const import CONF_MAC, STATE_HOME, STATE_NOT_HOME, STATE_UNAVAILABLE +from homeassistant.components.ruckus_unleashed import API_MAC, DOMAIN +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.device_registry import CONNECTION_NETWORK_MAC from homeassistant.util import utcnow from tests.async_mock import patch from tests.common import async_fire_time_changed from tests.components.ruckus_unleashed import ( + DEFAULT_AP_INFO, + DEFAULT_SYSTEM_INFO, DEFAULT_TITLE, + DEFAULT_UNIQUE_ID, TEST_CLIENT, TEST_CLIENT_ENTITY_ID, init_integration, @@ -23,17 +28,17 @@ async def test_client_connected(hass): future = utcnow() + timedelta(minutes=60) with patch( - "homeassistant.components.ruckus_unleashed.Ruckus.clients", + "homeassistant.components.ruckus_unleashed.RuckusUnleashedDataUpdateCoordinator._fetch_clients", return_value={ - TEST_CLIENT[CONF_MAC]: TEST_CLIENT, + TEST_CLIENT[API_MAC]: TEST_CLIENT, }, ): async_fire_time_changed(hass, future) await hass.async_block_till_done() - 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): @@ -42,7 +47,7 @@ async def test_client_disconnected(hass): future = utcnow() + timedelta(minutes=60) with patch( - "homeassistant.components.ruckus_unleashed.Ruckus.clients", + "homeassistant.components.ruckus_unleashed.RuckusUnleashedDataUpdateCoordinator._fetch_clients", return_value={}, ): async_fire_time_changed(hass, future) @@ -59,7 +64,7 @@ async def test_clients_update_failed(hass): future = utcnow() + timedelta(minutes=60) with patch( - "homeassistant.components.ruckus_unleashed.Ruckus.clients", + "homeassistant.components.ruckus_unleashed.RuckusUnleashedDataUpdateCoordinator._fetch_clients", side_effect=ConnectionError, ): async_fire_time_changed(hass, future) @@ -79,7 +84,7 @@ async def test_restoring_clients(hass): registry.async_get_or_create( "device_tracker", DOMAIN, - TEST_CLIENT[CONF_MAC], + DEFAULT_UNIQUE_ID, suggested_object_id="ruckus_test_device", config_entry=entry, ) @@ -91,7 +96,13 @@ async def test_restoring_clients(hass): "homeassistant.components.ruckus_unleashed.Ruckus.mesh_name", return_value=DEFAULT_TITLE, ), 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={}, ): entry.add_to_hass(hass) @@ -101,3 +112,24 @@ async def test_restoring_clients(hass): device = hass.states.get(TEST_CLIENT_ENTITY_ID) assert device is not None 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 diff --git a/tests/components/ruckus_unleashed/test_init.py b/tests/components/ruckus_unleashed/test_init.py index 3c12e4f9665..5c694a5ee24 100644 --- a/tests/components/ruckus_unleashed/test_init.py +++ b/tests/components/ruckus_unleashed/test_init.py @@ -1,15 +1,32 @@ """Test the Ruckus Unleashed config flow.""" 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 ( ENTRY_STATE_LOADED, ENTRY_STATE_NOT_LOADED, ENTRY_STATE_SETUP_RETRY, ) +from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC 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): @@ -40,6 +57,26 @@ async def test_setup_entry_connection_error(hass): 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): """Test successful unload of entry.""" entry = await init_integration(hass) @@ -52,3 +89,26 @@ async def test_unload_entry(hass): assert entry.state == ENTRY_STATE_NOT_LOADED 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