Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 26 additions & 3 deletions homeassistant/components/glances/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
GlancesApiError,
GlancesApiNoDataAvailable,
)
import httpx

from homeassistant.const import (
CONF_HOST,
Expand All @@ -19,17 +20,23 @@
CONF_VERIFY_SSL,
Platform,
)
from homeassistant.core import HomeAssistant
from homeassistant.core import HomeAssistant, callback
from homeassistant.exceptions import (
ConfigEntryAuthFailed,
ConfigEntryError,
ConfigEntryNotReady,
HomeAssistantError,
)
from homeassistant.helpers.httpx_client import get_async_client
from homeassistant.helpers.httpx_client import create_async_httpx_client
from homeassistant.util.hass_dict import HassKey

from .const import DEFAULT_TIMEOUT
from .coordinator import GlancesConfigEntry, GlancesDataUpdateCoordinator

DATA_HTTPX_CLIENT: HassKey[dict[bool, httpx.AsyncClient]] = HassKey(
"glances_httpx_client"
)

PLATFORMS = [Platform.SENSOR]


Expand Down Expand Up @@ -63,9 +70,25 @@ async def async_unload_entry(hass: HomeAssistant, entry: GlancesConfigEntry) ->
return await hass.config_entries.async_unload_platforms(entry, PLATFORMS)


@callback
def _async_get_httpx_client(hass: HomeAssistant, verify_ssl: bool) -> httpx.AsyncClient:
"""Return a cached Glances httpx client (one per verify_ssl value).

The shared httpx client cannot be used because it has a 5-second timeout
that is too short for slow Glances hosts. Caching here ensures entry
reloads reuse the same client instead of leaking one on every reload.
"""
clients = hass.data.setdefault(DATA_HTTPX_CLIENT, {})
if (client := clients.get(verify_ssl)) is None:
client = clients[verify_ssl] = create_async_httpx_client(
hass, verify_ssl=verify_ssl, timeout=DEFAULT_TIMEOUT
)
return client


async def get_api(hass: HomeAssistant, entry_data: dict[str, Any]) -> Glances:
"""Return the api from glances_api."""
httpx_client = get_async_client(hass, verify_ssl=entry_data[CONF_VERIFY_SSL])
httpx_client = _async_get_httpx_client(hass, entry_data[CONF_VERIFY_SSL])
for version in (4, 3):
api = Glances(
host=entry_data[CONF_HOST],
Expand Down
1 change: 1 addition & 0 deletions homeassistant/components/glances/const.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,5 +9,6 @@
DEFAULT_HOST = "localhost"
DEFAULT_PORT = 61208
DEFAULT_SCAN_INTERVAL = timedelta(seconds=60)
DEFAULT_TIMEOUT = 30

CPU_ICON = f"mdi:cpu-{64 if sys.maxsize > 2**32 else 32}-bit"
2 changes: 1 addition & 1 deletion homeassistant/components/glances/coordinator.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ async def _async_update_data(self) -> dict[str, Any]:
except exceptions.GlancesApiAuthorizationError as err:
raise ConfigEntryAuthFailed from err
except exceptions.GlancesApiError as err:
raise UpdateFailed from err
raise UpdateFailed(str(err)) from err
# Update computed values
uptime: datetime | None = None
up_duration: timedelta | None = None
Expand Down
64 changes: 61 additions & 3 deletions tests/components/glances/test_init.py
Original file line number Diff line number Diff line change
@@ -1,21 +1,27 @@
"""Tests for Glances integration."""

from unittest.mock import MagicMock
from unittest.mock import MagicMock, patch

from freezegun.api import FrozenDateTimeFactory
from glances_api.exceptions import (
GlancesApiAuthorizationError,
GlancesApiConnectionError,
GlancesApiNoDataAvailable,
)
import pytest

from homeassistant.components.glances.const import DOMAIN
from homeassistant.components.glances.const import (
DEFAULT_SCAN_INTERVAL,
DEFAULT_TIMEOUT,
DOMAIN,
)
from homeassistant.config_entries import ConfigEntryState
from homeassistant.core import HomeAssistant
from homeassistant.helpers.update_coordinator import UpdateFailed

from . import MOCK_USER_INPUT

from tests.common import MockConfigEntry
from tests.common import MockConfigEntry, async_fire_time_changed


async def test_successful_config_entry(hass: HomeAssistant) -> None:
Expand Down Expand Up @@ -53,6 +59,58 @@ async def test_setup_error(
assert entry.state is entry_state


async def test_update_error_includes_message(
hass: HomeAssistant,
freezer: FrozenDateTimeFactory,
mock_api: MagicMock,
) -> None:
"""Test that the underlying API error message is propagated to UpdateFailed."""
entry = MockConfigEntry(domain=DOMAIN, data=MOCK_USER_INPUT)
entry.add_to_hass(hass)

await hass.config_entries.async_setup(entry.entry_id)
Comment on lines +62 to +71
await hass.async_block_till_done()
assert entry.state is ConfigEntryState.LOADED

mock_api.return_value.get_ha_sensor_data.side_effect = GlancesApiConnectionError(
"Connection to http://localhost:61209/api/4/all failed"
)
freezer.tick(DEFAULT_SCAN_INTERVAL)
async_fire_time_changed(hass)
await hass.async_block_till_done()

coordinator = entry.runtime_data
assert coordinator.last_update_success is False
assert isinstance(coordinator.last_exception, UpdateFailed)
assert "Connection to http://localhost:61209/api/4/all failed" in str(
coordinator.last_exception
)


async def test_dedicated_httpx_client_uses_timeout_and_is_cached(
hass: HomeAssistant,
) -> None:
"""The integration's dedicated httpx client uses DEFAULT_TIMEOUT and is reused on reload."""
entry = MockConfigEntry(domain=DOMAIN, data=MOCK_USER_INPUT)
entry.add_to_hass(hass)

with patch(
"homeassistant.components.glances.create_async_httpx_client"
) as mock_create:
await hass.config_entries.async_setup(entry.entry_id)
await hass.async_block_till_done()
assert entry.state is ConfigEntryState.LOADED

assert mock_create.call_count == 1
kwargs = mock_create.call_args.kwargs
assert kwargs["timeout"] == DEFAULT_TIMEOUT
assert kwargs["verify_ssl"] == MOCK_USER_INPUT["verify_ssl"]

assert await hass.config_entries.async_reload(entry.entry_id)
await hass.async_block_till_done()
assert mock_create.call_count == 1


async def test_unload_entry(hass: HomeAssistant) -> None:
"""Test removing Glances."""
entry = MockConfigEntry(domain=DOMAIN, data=MOCK_USER_INPUT)
Expand Down
Loading