Skip to content

Commit 8ac85e2

Browse files
committed
Rebase PR #253 onto main: add keep_keys, get_logs, speed test, LoginConnectionException, SpeedTestResult
1 parent a46d319 commit 8ac85e2

3 files changed

Lines changed: 84 additions & 4 deletions

File tree

sagemcom_api/client.py

Lines changed: 56 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@
4444
AuthenticationException,
4545
BadRequestException,
4646
InvalidSessionException,
47+
LoginConnectionException,
4748
LoginRetryErrorException,
4849
LoginTimeoutException,
4950
MaximumSessionCountException,
@@ -53,7 +54,7 @@
5354
UnknownPathException,
5455
UnsupportedHostException,
5556
)
56-
from .models import Device, DeviceInfo, PortMapping
57+
from .models import Device, DeviceInfo, PortMapping, SpeedTestResult
5758

5859

5960
async def retry_login(invocation: Mapping[str, Any]) -> None:
@@ -77,6 +78,7 @@ def __init__(
7778
session: ClientSession | None = None,
7879
ssl: bool | None = False,
7980
verify_ssl: bool | None = True,
81+
keep_keys: bool = False,
8082
):
8183
"""Create a SagemCom client.
8284
@@ -85,11 +87,13 @@ def __init__(
8587
:param password: the password for your Sagemcom router
8688
:param authentication_method: the auth method of your Sagemcom router
8789
:param session: use a custom session, for example to configure the timeout
90+
:param keep_keys: return response keys as originally written (no snake_case conversion)
8891
"""
8992
self.host = host
9093
self.username = username
9194
self.authentication_method = authentication_method
9295
self.password = password
96+
self.keep_keys = keep_keys
9397
self._current_nonce = None
9498
self._password_hash = self.__generate_hash(password)
9599
self.protocol = "https" if ssl else "http"
@@ -191,7 +195,7 @@ def __get_response(self, response, index=0):
191195

192196
return value
193197

194-
def __get_response_value(self, response, index=0):
198+
def __get_response_value(self, response, index=0, keep_keys: bool | None = None):
195199
"""Retrieve response value from value."""
196200
try:
197201
value = self.__get_response(response, index)["value"]
@@ -200,8 +204,9 @@ def __get_response_value(self, response, index=0):
200204
except IndexError:
201205
value = None
202206

203-
# Rewrite result to snake_case
204-
if value is not None:
207+
# Rewrite result to snake_case unless keep_keys is requested
208+
should_keep = keep_keys if keep_keys is not None else self.keep_keys
209+
if value is not None and not should_keep:
205210
value = humps.decamelize(value)
206211

207212
return value
@@ -335,6 +340,8 @@ async def login(self):
335340
raise LoginTimeoutException(
336341
"Login request timed-out. This could be caused by using the wrong encryption method, or using a (non) SSL connection."
337342
) from exception
343+
except (ClientConnectorError, ClientOSError) as exception:
344+
raise LoginConnectionException("Unable to connect to the device. Please check the host address.") from exception
338345

339346
data = self.__get_response(response)
340347

@@ -552,6 +559,22 @@ async def get_port_mappings(self) -> list[PortMapping]:
552559
max_tries=1,
553560
on_backoff=retry_login,
554561
)
562+
async def get_logs(self) -> str:
563+
"""Retrieve system logs from Sagemcom F@st device."""
564+
actions = {
565+
"id": 0,
566+
"method": "getVendorLogDownloadURI",
567+
"xpath": urllib.parse.quote("Device/DeviceInfo/VendorLogFiles/VendorLogFile[@uid='1']"),
568+
}
569+
570+
response = await self.__api_request_async([actions], False)
571+
log_path = response["reply"]["actions"][0]["callbacks"][0]["parameters"]["uri"]
572+
573+
log_uri = f"{self.protocol}://{self.host}{log_path}"
574+
log_response = await self.session.get(log_uri)
575+
576+
return await log_response.text()
577+
555578
async def reboot(self):
556579
"""Reboot Sagemcom F@st device."""
557580
action = {
@@ -565,3 +588,32 @@ async def reboot(self):
565588
data = self.__get_response_value(response)
566589

567590
return data
591+
592+
async def run_speed_test(self, block_traffic: bool = False):
593+
"""Run Speed Test on Sagemcom F@st device."""
594+
actions = [
595+
{
596+
"id": 0,
597+
"method": "speedTestClient",
598+
"xpath": "Device/IP/Diagnostics/SpeedTest",
599+
"parameters": {"BlockTraffic": block_traffic},
600+
}
601+
]
602+
return await self.__api_request_async(actions, False)
603+
604+
async def get_speed_test_results(self) -> list[SpeedTestResult]:
605+
"""Retrieve Speed Test results from Sagemcom F@st device."""
606+
ret = await self.get_value_by_xpath("Device/IP/Diagnostics/SpeedTest")
607+
history = ret["speed_test"]["history"]
608+
if history:
609+
timestamps = (int(k) for k in history["timestamp"].split(","))
610+
server_address = history["selected_server_address"].split(",")
611+
block_traffic = history["block_traffic"].split(",")
612+
latency = history["latency"].split(",")
613+
upload = (float(k) for k in history["upload"].split(","))
614+
download = (float(k) for k in history["download"].split(","))
615+
results = [
616+
SpeedTestResult(*data) for data in zip(timestamps, server_address, block_traffic, latency, upload, download, strict=True)
617+
]
618+
return results
619+
return []

sagemcom_api/exceptions.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,10 @@ class LoginTimeoutException(BaseSagemcomException):
4343
"""Raised when a timeout is encountered during login."""
4444

4545

46+
class LoginConnectionException(BaseSagemcomException):
47+
"""Raised when a connection error is encountered during login."""
48+
49+
4650
class NonWritableParameterException(BaseSagemcomException):
4751
"""Raised when provided parameter is not writable."""
4852

sagemcom_api/models.py

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
"""Models for the Sagemcom F@st client."""
22

33
import dataclasses
4+
import time
45
from dataclasses import dataclass
56
from typing import Any
67

@@ -164,3 +165,26 @@ def __init__(self, **kwargs):
164165
def id(self):
165166
"""Return unique ID for port mapping."""
166167
return self.uid
168+
169+
170+
@dataclass
171+
class SpeedTestResult:
172+
"""Representation of a speedtest result."""
173+
174+
timestamp: int | str
175+
selected_server_address: str
176+
block_traffic: bool
177+
latency: str
178+
upload: float
179+
download: float
180+
181+
def __post_init__(self):
182+
"""Process data after init."""
183+
# Convert timestamp to human-readable string.
184+
if isinstance(self.timestamp, int):
185+
self.timestamp = time.strftime("%Y-%m-%d %H:%M:%S", time.localtime(self.timestamp))
186+
self.block_traffic = bool(self.block_traffic)
187+
188+
def __str__(self) -> str:
189+
"""Return string representation of speedtest result."""
190+
return f"timestamp: {self.timestamp}, latency: {self.latency}, upload: {self.upload}, download: {self.download}"

0 commit comments

Comments
 (0)