Skip to content

Commit 13fe222

Browse files
engrbm87fabaff
andauthored
Add method to retrive data for HA sensors (#11)
* add gitignore * Add strict typing Initialize self.data, Check for bad request * revert changes to httpx variable * update tests * add helper method `get_ha_sensor_data` * add tests * Update docstring Co-authored-by: Fabian Affolter <[email protected]>
1 parent 1bf3254 commit 13fe222

File tree

2 files changed

+204
-28
lines changed

2 files changed

+204
-28
lines changed

glances_api/__init__.py

Lines changed: 90 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,44 +1,44 @@
11
"""Client to interact with the Glances API."""
2-
import asyncio
2+
from __future__ import annotations
3+
34
import logging
5+
from typing import Any
46

57
import httpx
68

79
from . import exceptions
810

911
_LOGGER = logging.getLogger(__name__)
10-
_RESOURCE = "{schema}://{host}:{port}/api/{version}"
1112

1213

13-
class Glances(object):
14+
class Glances:
1415
"""A class for handling the data retrieval."""
1516

1617
def __init__(
1718
self,
18-
host="localhost",
19-
port=61208,
20-
version=2,
21-
ssl=False,
22-
verify_ssl=True,
23-
username=None,
24-
password=None,
25-
httpx_client=None,
19+
host: str = "localhost",
20+
port: int = 61208,
21+
version: int = 2,
22+
ssl: bool = False,
23+
verify_ssl: bool = True,
24+
username: str | None = None,
25+
password: str | None = None,
26+
httpx_client: httpx.AsyncClient | None = None,
2627
):
2728
"""Initialize the connection."""
2829
schema = "https" if ssl else "http"
29-
self.url = _RESOURCE.format(
30-
schema=schema, host=host, port=port, version=version
31-
)
32-
self.plugins = None
33-
self.values = None
30+
self.url = f"{schema}://{host}:{port}/api/{version}"
31+
self.data: dict[str, Any] = {}
32+
self.plugins: list[str] = []
33+
self.values: Any | None = None
3434
self.username = username
3535
self.password = password
3636
self.verify_ssl = verify_ssl
3737
self.httpx_client = httpx_client
3838

39-
async def get_data(self, endpoint):
39+
async def get_data(self, endpoint: str) -> None:
4040
"""Retrieve the data."""
41-
url = "{}/{}".format(self.url, endpoint)
41+
url = f"{self.url}/{endpoint}"
4242

4343
httpx_client = (
4444
self.httpx_client
@@ -49,10 +49,10 @@ async def get_data(self, endpoint):
4949
try:
5050
async with httpx_client as client:
5151
if self.password is None:
52-
response = await client.get(str(url))
52+
response = await client.get(url)
5353
else:
5454
response = await client.get(
55-
str(url), auth=(self.username, self.password)
55+
url, auth=(self.username, self.password)
5656
)
5757
except (httpx.ConnectError, httpx.TimeoutException):
5858
raise exceptions.GlancesApiConnectionError(f"Connection to {url} failed")
@@ -61,21 +61,24 @@ async def get_data(self, endpoint):
6161
raise exceptions.GlancesApiAuthorizationError(
6262
"Please check your credentials"
6363
)
64-
64+
if response.status_code == httpx.codes.BAD_REQUEST:
65+
raise exceptions.GlancesApiNoDataAvailable(
66+
f"endpoint: '{endpoint}' is not valid"
67+
)
6568
if response.status_code == httpx.codes.OK:
6669
try:
6770
_LOGGER.debug(response.json())
6871
if endpoint == "all":
6972
self.data = response.json()
70-
if endpoint == "pluginslist":
73+
elif endpoint == "pluginslist":
7174
self.plugins = response.json()
7275
except TypeError:
7376
_LOGGER.error("Can not load data from Glances")
7477
raise exceptions.GlancesApiConnectionError(
7578
"Unable to get the data from Glances"
7679
)
7780

78-
async def get_metrics(self, element):
81+
async def get_metrics(self, element: str) -> None:
7982
"""Get all the metrics for a monitored element."""
8083
await self.get_data("all")
8184
await self.get_data("pluginslist")
@@ -84,3 +87,67 @@ async def get_metrics(self, element):
8487
self.values = self.data[element]
8588
else:
8689
raise exceptions.GlancesApiError("Element data not available")
90+
91+
async def get_ha_sensor_data(self) -> dict[str, Any] | None:
92+
"""Create a dictionary with data for Home Assistant sensors."""
93+
await self.get_data("all")
94+
95+
sensor_data: dict[str, Any] = {}
96+
97+
if disks := self.data.get("fs"):
98+
sensor_data["fs"] = {}
99+
for disk in disks:
100+
disk_free = disk.get("free") or (disk["size"] - disk["used"])
101+
sensor_data["fs"][disk["mnt_point"]] = {
102+
"disk_use": round(disk["used"] / 1024**3, 1),
103+
"disk_use_percent": disk["percent"],
104+
"disk_free": round(disk_free / 1024**3, 1),
105+
}
106+
if data := self.data.get("sensors"):
107+
sensor_data["sensors"] = {}
108+
for sensor in data:
109+
sensor_data["sensors"][sensor["label"]] = {
110+
sensor["type"]: sensor["value"]
111+
}
112+
if data := self.data.get("mem"):
113+
sensor_data["mem"] = {
114+
"memory_use_percent": data["percent"],
115+
"memory_use": round(data["used"] / 1024**2, 1),
116+
"memory_free": round(data["free"] / 1024**2, 1),
117+
}
118+
if data := self.data.get("memswap"):
119+
sensor_data["memswap"] = {
120+
"swap_use_percent": data["percent"],
121+
"swap_use": round(data["used"] / 1024**3, 1),
122+
"swap_free": round(data["free"] / 1024**3, 1),
123+
}
124+
if data := self.data.get("load"):
125+
sensor_data["load"] = {
126+
"processor_load": data.get("min15")
127+
or self.data["cpu"]["total"] # to be checked
128+
}
129+
if data := self.data.get("processcount"):
130+
sensor_data["processcount"] = {
131+
"process_running": data["running"],
132+
"process_total": data["total"],
133+
"process_thread": data["thread"],
134+
"process_sleeping": data["sleeping"],
135+
}
136+
if data := self.data.get("quicklook"):
137+
sensor_data["cpu"] = {"cpu_use_percent": data["cpu"]}
138+
if "docker" in self.data and (data := self.data["docker"].get("containers")):
139+
active_containers = [
140+
container for container in data if container["Status"] == "running"
141+
]
142+
sensor_data["docker"] = {"docker_active": len(active_containers)}
143+
cpu_use = 0.0
144+
for container in active_containers:
145+
cpu_use += container["cpu"]["total"]
146+
sensor_data["docker"]["docker_cpu_use"] = round(cpu_use, 1)
147+
mem_use = 0.0
148+
for container in active_containers:
149+
mem_use += container["memory"]["usage"]
150+
sensor_data["docker"]["docker_memory_use"] = round(mem_use / 1024**2, 1)
151+
if data := self.data.get("raid"):
152+
sensor_data["raid"] = data
153+
return sensor_data

tests/test_responses.py

Lines changed: 114 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
"""Test the interaction with the Glances API."""
22
import pytest
3-
from pytest_httpx import HTTPXMock
4-
53
from glances_api import Glances
4+
from glances_api.exceptions import GlancesApiNoDataAvailable
5+
from pytest_httpx import HTTPXMock
66

77
PLUGINS_LIST_RESPONSE = [
88
"alert",
@@ -37,6 +37,85 @@
3737
"key": "disk_name",
3838
},
3939
],
40+
"docker": {
41+
"containers": [
42+
{
43+
"key": "name",
44+
"name": "container1",
45+
"Status": "running",
46+
"cpu": {"total": 50.94973493230174},
47+
"cpu_percent": 50.94973493230174,
48+
"memory": {
49+
"usage": 1120321536,
50+
"limit": 3976318976,
51+
"rss": 480641024,
52+
"cache": 580915200,
53+
"max_usage": 1309597696,
54+
},
55+
"memory_usage": 539406336,
56+
},
57+
{
58+
"key": "name",
59+
"name": "container2",
60+
"Status": "running",
61+
"cpu": {"total": 26.23567931034483},
62+
"cpu_percent": 26.23567931034483,
63+
"memory": {
64+
"usage": 85139456,
65+
"limit": 3976318976,
66+
"rss": 33677312,
67+
"cache": 35012608,
68+
"max_usage": 87650304,
69+
},
70+
"memory_usage": 50126848,
71+
},
72+
]
73+
},
74+
"fs": [
75+
{
76+
"device_name": "/dev/sda8",
77+
"fs_type": "ext4",
78+
"mnt_point": "/ssl",
79+
"size": 511320748032,
80+
"used": 32910458880,
81+
"free": 457917374464,
82+
"percent": 6.7,
83+
"key": "mnt_point",
84+
},
85+
{
86+
"device_name": "/dev/sda8",
87+
"fs_type": "ext4",
88+
"mnt_point": "/media",
89+
"size": 511320748032,
90+
"used": 32910458880,
91+
"free": 457917374464,
92+
"percent": 6.7,
93+
"key": "mnt_point",
94+
},
95+
],
96+
"mem": {
97+
"total": 3976318976,
98+
"available": 2878337024,
99+
"percent": 27.6,
100+
"used": 1097981952,
101+
"free": 2878337024,
102+
"active": 567971840,
103+
"inactive": 1679704064,
104+
"buffers": 149807104,
105+
"cached": 1334816768,
106+
"shared": 1499136,
107+
},
108+
"sensors": [
109+
{
110+
"label": "cpu_thermal 1",
111+
"value": 59,
112+
"warning": None,
113+
"critical": None,
114+
"unit": "C",
115+
"type": "temperature_core",
116+
"key": "label",
117+
}
118+
],
40119
"system": {
41120
"os_name": "Linux",
42121
"hostname": "fedora-35",
@@ -48,16 +127,35 @@
48127
"uptime": "3 days, 10:25:20",
49128
}
50129

130+
HA_SENSOR_DATA = {
131+
"fs": {
132+
"/ssl": {"disk_use": 30.7, "disk_use_percent": 6.7, "disk_free": 426.5},
133+
"/media": {"disk_use": 30.7, "disk_use_percent": 6.7, "disk_free": 426.5},
134+
},
135+
"sensors": {"cpu_thermal 1": {"temperature_core": 59}},
136+
"mem": {
137+
"memory_use_percent": 27.6,
138+
"memory_use": 1047.1,
139+
"memory_free": 2745.0,
140+
},
141+
"docker": {
142+
"docker_active": 2,
143+
"docker_cpu_use": 77.2,
144+
"docker_memory_use": 1149.6
145+
}
146+
}
147+
51148

52149
@pytest.mark.asyncio
53150
async def test_non_existing_endpoint(httpx_mock: HTTPXMock):
54151
"""Test a non-exisiting endpoint."""
55-
httpx_mock.add_response(json={})
152+
httpx_mock.add_response(status_code=400)
56153

57154
client = Glances()
58-
await client.get_data("some-data")
155+
with pytest.raises(GlancesApiNoDataAvailable):
156+
await client.get_data("some-data")
59157

60-
assert client.values == None
158+
assert not client.data
61159

62160

63161
@pytest.mark.asyncio
@@ -81,3 +179,14 @@ async def test_exisiting_endpoint(httpx_mock: HTTPXMock):
81179

82180
assert client.values["total"] == 10.6
83181
assert client.values["system"] == 2.1
182+
183+
184+
@pytest.mark.asyncio
185+
async def test_ha_sensor_data(httpx_mock: HTTPXMock):
186+
"""Test the return value for ha sensors."""
187+
httpx_mock.add_response(json=RESPONSE)
188+
189+
client = Glances()
190+
result = await client.get_ha_sensor_data()
191+
192+
assert result == HA_SENSOR_DATA

0 commit comments

Comments
 (0)