diff --git a/homeassistant/components/glances/__init__.py b/homeassistant/components/glances/__init__.py index bda1baf797af..1c03f8c1dbfe 100644 --- a/homeassistant/components/glances/__init__.py +++ b/homeassistant/components/glances/__init__.py @@ -1,13 +1,34 @@ """The Glances component.""" +import logging from typing import Any from glances_api import Glances +from glances_api.exceptions import ( + GlancesApiAuthorizationError, + GlancesApiError, + GlancesApiNoDataAvailable, +) from homeassistant.config_entries import ConfigEntry -from homeassistant.const import CONF_NAME, CONF_VERIFY_SSL, Platform +from homeassistant.const import ( + CONF_HOST, + CONF_PASSWORD, + CONF_PORT, + CONF_SSL, + CONF_USERNAME, + CONF_VERIFY_SSL, + Platform, +) from homeassistant.core import HomeAssistant +from homeassistant.exceptions import ( + ConfigEntryAuthFailed, + ConfigEntryError, + ConfigEntryNotReady, + HomeAssistantError, +) import homeassistant.helpers.config_validation as cv from homeassistant.helpers.httpx_client import get_async_client +from homeassistant.helpers.issue_registry import IssueSeverity, async_create_issue from .const import DOMAIN from .coordinator import GlancesDataUpdateCoordinator @@ -16,10 +37,19 @@ PLATFORMS = [Platform.SENSOR] CONFIG_SCHEMA = cv.removed(DOMAIN, raise_if_present=False) +_LOGGER = logging.getLogger(__name__) + async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool: """Set up Glances from config entry.""" - api = get_api(hass, dict(config_entry.data)) + try: + api = await get_api(hass, dict(config_entry.data)) + except GlancesApiAuthorizationError as err: + raise ConfigEntryAuthFailed from err + except GlancesApiError as err: + raise ConfigEntryNotReady from err + except ServerVersionMismatch as err: + raise ConfigEntryError(err) from err coordinator = GlancesDataUpdateCoordinator(hass, config_entry, api) await coordinator.async_config_entry_first_refresh() @@ -39,8 +69,38 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: return unload_ok -def get_api(hass: HomeAssistant, entry_data: dict[str, Any]) -> Glances: +async def get_api(hass: HomeAssistant, entry_data: dict[str, Any]) -> Glances: """Return the api from glances_api.""" - entry_data.pop(CONF_NAME, None) httpx_client = get_async_client(hass, verify_ssl=entry_data[CONF_VERIFY_SSL]) - return Glances(httpx_client=httpx_client, **entry_data) + for version in (3, 2): + api = Glances( + host=entry_data[CONF_HOST], + port=entry_data[CONF_PORT], + version=version, + ssl=entry_data[CONF_SSL], + username=entry_data.get(CONF_USERNAME), + password=entry_data.get(CONF_PASSWORD), + httpx_client=httpx_client, + ) + try: + await api.get_ha_sensor_data() + except GlancesApiNoDataAvailable as err: + _LOGGER.debug("Failed to connect to Glances API v%s: %s", version, err) + continue + if version == 2: + async_create_issue( + hass, + DOMAIN, + "deprecated_version", + breaks_in_ha_version="2024.8.0", + is_fixable=False, + severity=IssueSeverity.WARNING, + translation_key="deprecated_version", + ) + _LOGGER.debug("Connected to Glances API v%s", version) + return api + raise ServerVersionMismatch("Could not connect to Glances API version 2 or 3") + + +class ServerVersionMismatch(HomeAssistantError): + """Raise exception if we fail to connect to Glances API.""" diff --git a/homeassistant/components/glances/config_flow.py b/homeassistant/components/glances/config_flow.py index 72555b629d7c..81d3a1187290 100644 --- a/homeassistant/components/glances/config_flow.py +++ b/homeassistant/components/glances/config_flow.py @@ -21,15 +21,8 @@ from homeassistant.const import ( ) from homeassistant.data_entry_flow import FlowResult -from . import get_api -from .const import ( - CONF_VERSION, - DEFAULT_HOST, - DEFAULT_PORT, - DEFAULT_VERSION, - DOMAIN, - SUPPORTED_VERSIONS, -) +from . import ServerVersionMismatch, get_api +from .const import DEFAULT_HOST, DEFAULT_PORT, DOMAIN DATA_SCHEMA = vol.Schema( { @@ -37,7 +30,6 @@ DATA_SCHEMA = vol.Schema( vol.Optional(CONF_USERNAME): str, vol.Optional(CONF_PASSWORD): str, vol.Required(CONF_PORT, default=DEFAULT_PORT): int, - vol.Required(CONF_VERSION, default=DEFAULT_VERSION): vol.In(SUPPORTED_VERSIONS), vol.Optional(CONF_SSL, default=False): bool, vol.Optional(CONF_VERIFY_SSL, default=False): bool, } @@ -65,9 +57,8 @@ class GlancesFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): assert self._reauth_entry if user_input is not None: user_input = {**self._reauth_entry.data, **user_input} - api = get_api(self.hass, user_input) try: - await api.get_ha_sensor_data() + await get_api(self.hass, user_input) except GlancesApiAuthorizationError: errors["base"] = "invalid_auth" except GlancesApiConnectionError: @@ -101,12 +92,11 @@ class GlancesFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): self._async_abort_entries_match( {CONF_HOST: user_input[CONF_HOST], CONF_PORT: user_input[CONF_PORT]} ) - api = get_api(self.hass, user_input) try: - await api.get_ha_sensor_data() + await get_api(self.hass, user_input) except GlancesApiAuthorizationError: errors["base"] = "invalid_auth" - except GlancesApiConnectionError: + except (GlancesApiConnectionError, ServerVersionMismatch): errors["base"] = "cannot_connect" else: return self.async_create_entry( diff --git a/homeassistant/components/glances/const.py b/homeassistant/components/glances/const.py index 37da60bdea80..f0477a304638 100644 --- a/homeassistant/components/glances/const.py +++ b/homeassistant/components/glances/const.py @@ -8,9 +8,6 @@ CONF_VERSION = "version" DEFAULT_HOST = "localhost" DEFAULT_PORT = 61208 -DEFAULT_VERSION = 3 DEFAULT_SCAN_INTERVAL = timedelta(seconds=60) -SUPPORTED_VERSIONS = [2, 3] - CPU_ICON = f"mdi:cpu-{64 if sys.maxsize > 2**32 else 32}-bit" diff --git a/homeassistant/components/glances/strings.json b/homeassistant/components/glances/strings.json index 1bab098d65f7..7e69e7f7912f 100644 --- a/homeassistant/components/glances/strings.json +++ b/homeassistant/components/glances/strings.json @@ -7,7 +7,6 @@ "username": "[%key:common::config_flow::data::username%]", "password": "[%key:common::config_flow::data::password%]", "port": "[%key:common::config_flow::data::port%]", - "version": "Glances API Version (2 or 3)", "ssl": "[%key:common::config_flow::data::ssl%]", "verify_ssl": "[%key:common::config_flow::data::verify_ssl%]" }, @@ -30,5 +29,11 @@ "already_configured": "[%key:common::config_flow::abort::already_configured_device%]", "reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]" } + }, + "issues": { + "deprecated_version": { + "title": "Glances servers with version 2 is deprecated", + "description": "Glances servers with version 2 is deprecated and will not be supported in future versions of HA. It is recommended to update your server to Glances version 3 then reload the integration." + } } } diff --git a/tests/components/glances/__init__.py b/tests/components/glances/__init__.py index 91f8da927995..f0f1fe017964 100644 --- a/tests/components/glances/__init__.py +++ b/tests/components/glances/__init__.py @@ -6,7 +6,6 @@ MOCK_USER_INPUT: dict[str, Any] = { "host": "0.0.0.0", "username": "username", "password": "password", - "version": 3, "port": 61208, "ssl": False, "verify_ssl": True, diff --git a/tests/components/glances/test_config_flow.py b/tests/components/glances/test_config_flow.py index 87ec80da057b..8d590317c619 100644 --- a/tests/components/glances/test_config_flow.py +++ b/tests/components/glances/test_config_flow.py @@ -4,6 +4,7 @@ from unittest.mock import MagicMock from glances_api.exceptions import ( GlancesApiAuthorizationError, GlancesApiConnectionError, + GlancesApiNoDataAvailable, ) import pytest @@ -47,6 +48,7 @@ async def test_form(hass: HomeAssistant) -> None: [ (GlancesApiAuthorizationError, "invalid_auth"), (GlancesApiConnectionError, "cannot_connect"), + (GlancesApiNoDataAvailable, "cannot_connect"), ], ) async def test_form_fails( @@ -54,7 +56,7 @@ async def test_form_fails( ) -> None: """Test flow fails when api exception is raised.""" - mock_api.return_value.get_ha_sensor_data.side_effect = [error, HA_SENSOR_DATA] + mock_api.return_value.get_ha_sensor_data.side_effect = error result = await hass.config_entries.flow.async_init( glances.DOMAIN, context={"source": config_entries.SOURCE_USER} ) @@ -65,12 +67,6 @@ async def test_form_fails( assert result["type"] == FlowResultType.FORM assert result["errors"] == {"base": message} - result = await hass.config_entries.flow.async_configure( - result["flow_id"], user_input=MOCK_USER_INPUT - ) - - assert result["type"] == FlowResultType.CREATE_ENTRY - async def test_form_already_configured(hass: HomeAssistant) -> None: """Test host is already configured.""" diff --git a/tests/components/glances/test_init.py b/tests/components/glances/test_init.py index 61cbc6100609..764426c6276f 100644 --- a/tests/components/glances/test_init.py +++ b/tests/components/glances/test_init.py @@ -1,17 +1,19 @@ """Tests for Glances integration.""" -from unittest.mock import MagicMock +from unittest.mock import AsyncMock, MagicMock from glances_api.exceptions import ( GlancesApiAuthorizationError, GlancesApiConnectionError, + GlancesApiNoDataAvailable, ) import pytest from homeassistant.components.glances.const import DOMAIN from homeassistant.config_entries import ConfigEntryState from homeassistant.core import HomeAssistant +from homeassistant.helpers import issue_registry as ir -from . import MOCK_USER_INPUT +from . import HA_SENSOR_DATA, MOCK_USER_INPUT from tests.common import MockConfigEntry @@ -27,11 +29,34 @@ async def test_successful_config_entry(hass: HomeAssistant) -> None: assert entry.state == ConfigEntryState.LOADED +async def test_entry_deprecated_version( + hass: HomeAssistant, issue_registry: ir.IssueRegistry, mock_api: AsyncMock +) -> None: + """Test creating an issue if glances server is version 2.""" + entry = MockConfigEntry(domain=DOMAIN, data=MOCK_USER_INPUT) + entry.add_to_hass(hass) + + mock_api.return_value.get_ha_sensor_data.side_effect = [ + GlancesApiNoDataAvailable("endpoint: 'all' is not valid"), + HA_SENSOR_DATA, + HA_SENSOR_DATA, + ] + + await hass.config_entries.async_setup(entry.entry_id) + + assert entry.state == ConfigEntryState.LOADED + + issue = issue_registry.async_get_issue(DOMAIN, "deprecated_version") + assert issue is not None + assert issue.severity == ir.IssueSeverity.WARNING + + @pytest.mark.parametrize( ("error", "entry_state"), [ (GlancesApiAuthorizationError, ConfigEntryState.SETUP_ERROR), (GlancesApiConnectionError, ConfigEntryState.SETUP_RETRY), + (GlancesApiNoDataAvailable, ConfigEntryState.SETUP_ERROR), ], ) async def test_setup_error(