From 07ef8e87eb5046e6e7cfa4132828ac4b5c9716ef Mon Sep 17 00:00:00 2001 From: M4D1NG3R <67385257+M4D1NG3R@users.noreply.github.com> Date: Wed, 16 Feb 2022 13:01:01 +0100 Subject: [PATCH 01/12] Fix for device_info JSON format --- sagemcom_api/client.py | 1 + 1 file changed, 1 insertion(+) diff --git a/sagemcom_api/client.py b/sagemcom_api/client.py index b8cc41e..4c90681 100644 --- a/sagemcom_api/client.py +++ b/sagemcom_api/client.py @@ -362,6 +362,7 @@ async def get_device_info(self) -> DeviceInfo: """Retrieve information about Sagemcom F@st device.""" try: data = await self.get_value_by_xpath("Device/DeviceInfo") + return DeviceInfo(**data.get("device_info")) except UnknownPathException: data = await self.get_values_by_xpaths( { From 31e7ed394834efce1f510af84ffbd27b193002e0 Mon Sep 17 00:00:00 2001 From: Tobias Bittner Date: Sat, 6 Aug 2022 16:20:27 +0200 Subject: [PATCH 02/12] Fixed problem from issue #183 The xpath in set_value_by_xpath is now also url encoded. Now requests with selectors (e.g. "some/xpath[Key='Value']") are possible without "Invalid format" errors. --- sagemcom_api/client.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sagemcom_api/client.py b/sagemcom_api/client.py index 4c90681..044b8ed 100644 --- a/sagemcom_api/client.py +++ b/sagemcom_api/client.py @@ -349,7 +349,7 @@ async def set_value_by_xpath( actions = { "id": 0, "method": "setValue", - "xpath": xpath, + "xpath": urllib.parse.quote(xpath), "parameters": {"value": str(value)}, "options": options, } From 3372214d63b7eff4fce1cb36a5d2cb850497300f Mon Sep 17 00:00:00 2001 From: djGrrr Date: Sun, 5 Mar 2023 19:58:27 -0330 Subject: [PATCH 03/12] Add keep_keys setting for returning keys as they are originally written --- sagemcom_api/client.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/sagemcom_api/client.py b/sagemcom_api/client.py index 044b8ed..5c024cb 100644 --- a/sagemcom_api/client.py +++ b/sagemcom_api/client.py @@ -55,6 +55,7 @@ def __init__( session: ClientSession = None, ssl=False, verify_ssl=True, + keep_keys=False, ): """ Create a SagemCom client. @@ -69,6 +70,7 @@ def __init__( self.username = username self.authentication_method = authentication_method self._password_hash = self.__generate_hash(password) + self.keep_keys = keep_keys self.protocol = "https" if ssl else "http" @@ -164,7 +166,8 @@ def __get_response_value(self, response, index=0): value = None # Rewrite result to snake_case - value = humps.decamelize(value) + if not self.keep_keys: + value = humps.decamelize(value) return value From 1923c580d1f9a140bf106fee912557c2adb00424 Mon Sep 17 00:00:00 2001 From: djGrrr Date: Sun, 5 Mar 2023 22:18:41 -0330 Subject: [PATCH 04/12] Don't break built-in apis when keep_keys is True --- sagemcom_api/client.py | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/sagemcom_api/client.py b/sagemcom_api/client.py index 5c024cb..e650dec 100644 --- a/sagemcom_api/client.py +++ b/sagemcom_api/client.py @@ -158,7 +158,7 @@ def __get_response(self, response, index=0): return value - def __get_response_value(self, response, index=0): + def __get_response_value(self, response, index=0, keep_keys = None): """Retrieve response value from value.""" try: value = self.__get_response(response, index)["value"] @@ -166,7 +166,7 @@ def __get_response_value(self, response, index=0): value = None # Rewrite result to snake_case - if not self.keep_keys: + if (keep_keys is not None and not keep_keys) or (keep_keys is None and not self.keep_keys): value = humps.decamelize(value) return value @@ -296,7 +296,7 @@ async def logout(self): self._request_id = -1 async def get_value_by_xpath( - self, xpath: str, options: Optional[Dict] = {} + self, xpath: str, options: Optional[Dict] = {}, keep_keys = None ) -> Dict: """ Retrieve raw value from router using XPath. @@ -312,11 +312,11 @@ async def get_value_by_xpath( } response = await self.__api_request_async([actions], False) - data = self.__get_response_value(response) + data = self.__get_response_value(response, keep_keys = keep_keys) return data - async def get_values_by_xpaths(self, xpaths, options: Optional[Dict] = {}) -> Dict: + async def get_values_by_xpaths(self, xpaths, options: Optional[Dict] = {}, keep_keys = None) -> Dict: """ Retrieve raw values from router using XPath. @@ -334,7 +334,7 @@ async def get_values_by_xpaths(self, xpaths, options: Optional[Dict] = {}) -> Di ] response = await self.__api_request_async(actions, False) - values = [self.__get_response_value(response, i) for i in range(len(xpaths))] + values = [self.__get_response_value(response, i, keep_keys = keep_keys) for i in range(len(xpaths))] data = dict(zip(xpaths.keys(), values)) return data @@ -364,7 +364,7 @@ async def set_value_by_xpath( async def get_device_info(self) -> DeviceInfo: """Retrieve information about Sagemcom F@st device.""" try: - data = await self.get_value_by_xpath("Device/DeviceInfo") + data = await self.get_value_by_xpath("Device/DeviceInfo", keep_keys = False) return DeviceInfo(**data.get("device_info")) except UnknownPathException: data = await self.get_values_by_xpaths( @@ -383,7 +383,7 @@ async def get_device_info(self) -> DeviceInfo: async def get_hosts(self, only_active: Optional[bool] = False) -> List[Device]: """Retrieve hosts connected to Sagemcom F@st device.""" - data = await self.get_value_by_xpath("Device/Hosts/Hosts") + data = await self.get_value_by_xpath("Device/Hosts/Hosts", keep_keys = False) devices = [Device(**d) for d in data] if only_active: @@ -394,7 +394,7 @@ async def get_hosts(self, only_active: Optional[bool] = False) -> List[Device]: async def get_port_mappings(self) -> List[PortMapping]: """Retrieve configured Port Mappings on Sagemcom F@st device.""" - data = await self.get_value_by_xpath("Device/NAT/PortMappings") + data = await self.get_value_by_xpath("Device/NAT/PortMappings", keep_keys = False) port_mappings = [PortMapping(**p) for p in data] return port_mappings @@ -408,6 +408,6 @@ async def reboot(self): } response = await self.__api_request_async([action], False) - data = self.__get_response_value(response) + data = self.__get_response_value(response, keep_keys = False) return data From 2563bebe4fbbe8322a72f369d3ab771189e5f40f Mon Sep 17 00:00:00 2001 From: djGrrr Date: Sun, 5 Mar 2023 22:19:11 -0330 Subject: [PATCH 05/12] Update README with Sagemcom F@st 5689E support (Bell Giga Hub) --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 47349ac..34f8d83 100644 --- a/README.md +++ b/README.md @@ -28,6 +28,7 @@ The Sagemcom F@st series is used by multiple cable companies, where some cable c | Sagemcom F@st 5370e | Telia | sha512 | | | Sagemcom F@st 5566 | Bell (Home Hub 3000) | md5 | username: guest, password: "" | | Sagemcom F@st 5689 | Bell (Home Hub 4000) | md5 | username: admin, password: "" | +| Sagemcom F@st 5689E | Bell (Giga Hub) | sha512 | username: admin, password: "" | | Sagemcom F@st 5655V2 | MásMóvil | md5 | | | Sagemcom F@st 5657IL | | md5 | | | Speedport Pro | Telekom | md5 | username: admin | From 67fe20c41ea7e61e8ee66230d7a63ae9f4c65a43 Mon Sep 17 00:00:00 2001 From: andrewkreuzer Date: Sat, 25 Mar 2023 10:41:58 -0400 Subject: [PATCH 06/12] Add get_logs function to client to return system logs --- sagemcom_api/client.py | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/sagemcom_api/client.py b/sagemcom_api/client.py index 4c90681..a732f05 100644 --- a/sagemcom_api/client.py +++ b/sagemcom_api/client.py @@ -396,6 +396,25 @@ async def get_port_mappings(self) -> List[PortMapping]: return port_mappings + async def get_logs(self) -> List[str]: + """ + Retrieve system logs. + """ + + actions = { + "id": 0, + "method": "getVendorLogDownloadURI", + "xpath": urllib.parse.quote("Device/DeviceInfo/VendorLogFiles/VendorLogFile[@uid='1']"), + } + + response = await self.__api_request_async([actions], False) + log_path = response["reply"]["actions"][0]["callbacks"][0]["parameters"]["uri"] + + log_uri = f"{self.protocol}://{self.host}{log_path}" + response = await self.session.get(log_uri, timeout=10) + + return await response.text() + async def reboot(self): """Reboot Sagemcom F@st device.""" action = { From cdafe8d259b6609bfc8317d1362127ff7fe9d5db Mon Sep 17 00:00:00 2001 From: Alexandre Hassan Date: Tue, 4 Apr 2023 18:37:32 -0400 Subject: [PATCH 07/12] flake8 pre-commit has been moved, isort broken on macos --- .gitignore | 1 + .pre-commit-config.yaml | 4 ++-- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/.gitignore b/.gitignore index b6e4761..5d039d2 100644 --- a/.gitignore +++ b/.gitignore @@ -127,3 +127,4 @@ dmypy.json # Pyre type checker .pyre/ +.vscode/ \ No newline at end of file diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 0859882..0be6386 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -20,7 +20,7 @@ repos: - --skip="./.*,*.csv,*.json,*.md" - --quiet-level=2 exclude_types: [csv, json] - - repo: https://gitlab.com/pycqa/flake8 + - repo: https://github.com/pycqa/flake8 rev: 3.8.4 hooks: - id: flake8 @@ -34,6 +34,6 @@ repos: hooks: - id: yamllint - repo: https://github.com/PyCQA/isort - rev: 5.5.3 + rev: 5.12.0 hooks: - id: isort From 27c8bc8cf5f14e8424d823e6252479fea94e7458 Mon Sep 17 00:00:00 2001 From: Alexandre Hassan Date: Tue, 4 Apr 2023 19:31:58 -0400 Subject: [PATCH 08/12] add option to send speed tests and get results --- sagemcom_api/client.py | 40 +++++++++++++++++++++++++++++++++++++--- sagemcom_api/models.py | 28 ++++++++++++++++++++++++++++ 2 files changed, 65 insertions(+), 3 deletions(-) diff --git a/sagemcom_api/client.py b/sagemcom_api/client.py index 4c90681..06b199e 100644 --- a/sagemcom_api/client.py +++ b/sagemcom_api/client.py @@ -40,7 +40,7 @@ UnknownException, UnknownPathException, ) -from .models import Device, DeviceInfo, PortMapping +from .models import Device, DeviceInfo, PortMapping, SpeedTestResult class SagemcomClient: @@ -168,7 +168,7 @@ def __get_response_value(self, response, index=0): return value - async def __api_request_async(self, actions, priority=False): + async def __api_request_async(self, actions, priority=False, **request_kwargs): """Build request to the internal JSON-req API.""" self.__generate_request_id() self.__generate_nonce() @@ -188,7 +188,9 @@ async def __api_request_async(self, actions, priority=False): } async with self.session.post( - api_host, data="req=" + json.dumps(payload, separators=(",", ":")) + api_host, + data="req=" + json.dumps(payload, separators=(",", ":")), + **request_kwargs, ) as response: if response.status == 400: @@ -408,3 +410,35 @@ async def reboot(self): data = self.__get_response_value(response) return data + + async def run_speed_test(self, block_traffic: bool = False): + """Run Speed Test on Sagemcom F@st device.""" + actions = [ + { + "id": 0, + "method": "speedTestClient", + "xpath": "Device/IP/Diagnostics/SpeedTest", + "parameters": {"BlockTraffic": block_traffic}, + } + ] + return await self.__api_request_async(actions, False, timeout=100) + + async def get_speed_test_results(self): + """Retrieve Speed Test results from Sagemcom F@st device.""" + ret = await self.get_value_by_xpath("Device/IP/Diagnostics/SpeedTest") + history = ret["speed_test"]["history"] + if history: + timestamps = (int(k) for k in history["timestamp"].split(",")) + server_address = history["selected_server_address"].split(",") + block_traffic = history["block_traffic"].split(",") + latency = history["latency"].split(",") + upload = (float(k) for k in history["upload"].split(",")) + download = (float(k) for k in history["download"].split(",")) + results = [ + SpeedTestResult(*data) + for data in zip( + timestamps, server_address, block_traffic, latency, upload, download + ) + ] + return results + return [] diff --git a/sagemcom_api/models.py b/sagemcom_api/models.py index 979049c..5b0c37e 100644 --- a/sagemcom_api/models.py +++ b/sagemcom_api/models.py @@ -2,6 +2,7 @@ import dataclasses from dataclasses import dataclass +import time from typing import Any, List, Optional @@ -162,3 +163,30 @@ def __init__(self, **kwargs): def id(self): """Return unique ID for port mapping.""" return self.uid + + +@dataclass +class SpeedTestResult: + """Representation of a speedtest result.""" + + timestamp: str + selected_server_address: str + block_traffic: bool + latency: str + upload: str + download: str + + def __post_init__(self): + """Process data after init.""" + # Convert timestamp to datetime object. + self.timestamp = time.strftime( + "%Y-%m-%d %H:%M:%S", time.localtime(self.timestamp) + ) + self.block_traffic = bool(self.block_traffic) + + def __str__(self) -> str: + """Return string representation of speedtest result.""" + return ( + f"timestamp: {self.timestamp}, latency: {self.latency}, " + f"upload: {self.upload}, download: {self.download}" + ) From 9f2dd0fb8c8159d228996756b97f45f62f96c15d Mon Sep 17 00:00:00 2001 From: Alexandre Hassan Date: Tue, 4 Apr 2023 19:37:59 -0400 Subject: [PATCH 09/12] flake8 pre-commit has been moved, isort broken on macos --- .gitignore | 1 + .pre-commit-config.yaml | 4 ++-- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/.gitignore b/.gitignore index b6e4761..5d039d2 100644 --- a/.gitignore +++ b/.gitignore @@ -127,3 +127,4 @@ dmypy.json # Pyre type checker .pyre/ +.vscode/ \ No newline at end of file diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 0859882..0be6386 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -20,7 +20,7 @@ repos: - --skip="./.*,*.csv,*.json,*.md" - --quiet-level=2 exclude_types: [csv, json] - - repo: https://gitlab.com/pycqa/flake8 + - repo: https://github.com/pycqa/flake8 rev: 3.8.4 hooks: - id: flake8 @@ -34,6 +34,6 @@ repos: hooks: - id: yamllint - repo: https://github.com/PyCQA/isort - rev: 5.5.3 + rev: 5.12.0 hooks: - id: isort From a40a61883db9b205f5a2e6b002db7e2be80ccbb5 Mon Sep 17 00:00:00 2001 From: Alexandre Hassan Date: Tue, 4 Apr 2023 19:38:21 -0400 Subject: [PATCH 10/12] Add bell giga hub and auth method --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 47349ac..e860382 100644 --- a/README.md +++ b/README.md @@ -28,6 +28,7 @@ The Sagemcom F@st series is used by multiple cable companies, where some cable c | Sagemcom F@st 5370e | Telia | sha512 | | | Sagemcom F@st 5566 | Bell (Home Hub 3000) | md5 | username: guest, password: "" | | Sagemcom F@st 5689 | Bell (Home Hub 4000) | md5 | username: admin, password: "" | +| Sagemcom F@st 5690 | Bell (Giga Hub) | sha512 | username: admin, password: "" | | Sagemcom F@st 5655V2 | MásMóvil | md5 | | | Sagemcom F@st 5657IL | | md5 | | | Speedport Pro | Telekom | md5 | username: admin | From 0ed1c45fd3cd8f7ad3de7affe77769aebef82f6e Mon Sep 17 00:00:00 2001 From: Alexandre Hassan Date: Tue, 4 Apr 2023 19:49:30 -0400 Subject: [PATCH 11/12] Add exception for un-reachable host --- sagemcom_api/client.py | 7 ++++++- sagemcom_api/exceptions.py | 6 ++++++ 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/sagemcom_api/client.py b/sagemcom_api/client.py index 4c90681..0cc61ad 100644 --- a/sagemcom_api/client.py +++ b/sagemcom_api/client.py @@ -10,7 +10,7 @@ from typing import Dict, List, Optional, Type import urllib.parse -from aiohttp import ClientSession, ClientTimeout +from aiohttp import ClientConnectionError, ClientSession, ClientTimeout from aiohttp.connector import TCPConnector import humps @@ -33,6 +33,7 @@ AccessRestrictionException, AuthenticationException, BadRequestException, + LoginConnectionException, LoginTimeoutException, MaximumSessionCountException, NonWritableParameterException, @@ -272,6 +273,10 @@ async def login(self): raise LoginTimeoutException( "Request timed-out. This is mainly due to using the wrong encryption method." ) from exception + except ClientConnectionError as exception: + raise LoginConnectionException( + "Unable to connect to the device. Please check the host address." + ) from exception data = self.__get_response(response) diff --git a/sagemcom_api/exceptions.py b/sagemcom_api/exceptions.py index 8c2eedd..c320c39 100644 --- a/sagemcom_api/exceptions.py +++ b/sagemcom_api/exceptions.py @@ -20,6 +20,12 @@ class LoginTimeoutException(Exception): pass +class LoginConnectionException(Exception): + """Raised when a connection error is encountered during login.""" + + pass + + class NonWritableParameterException(Exception): """Raised when provided parameter is not writable.""" From e596ab2d6056c349ded6a723c831e6538e80b2c6 Mon Sep 17 00:00:00 2001 From: sydragos <1521275+sydragos@users.noreply.github.com> Date: Sat, 9 Dec 2023 15:07:33 -0500 Subject: [PATCH 12/12] Update poetry.lock requests --- poetry.lock | 3 ++- pyproject.toml | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/poetry.lock b/poetry.lock index e23ac13..2b0fde7 100644 --- a/poetry.lock +++ b/poetry.lock @@ -14,6 +14,7 @@ charset-normalizer = ">=2.0,<3.0" frozenlist = ">=1.1.1" multidict = ">=4.5,<7.0" yarl = ">=1.0,<2.0" +requests = ">2.0" [package.extras] speedups = ["aiodns", "brotli", "cchardet"] @@ -425,7 +426,7 @@ py = ">=1.8.2" tomli = ">=1.0.0" [package.extras] -testing = ["argcomplete", "hypothesis (>=3.56)", "mock", "nose", "pygments (>=2.7.2)", "requests", "xmlschema"] +testing = ["argcomplete", "hypothesis (>=3.56)", "mock", "nose", "pygments (>=2.7.2)", "xmlschema"] [[package]] name = "pyupgrade" diff --git a/pyproject.toml b/pyproject.toml index 1f41e03..ad23294 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "sagemcom_api" -version = "1.0.8" +version = "1.0.9" description = "Python client to interact with SagemCom F@st routers via internal API's." authors = ["Mick Vleeshouwer "] license = "MIT" @@ -20,6 +20,7 @@ packages = [ python = ">=3.9,<4.0" aiohttp = "^3.7.3" pyhumps = "^3.0.2" +requests = ">2.0" [tool.poetry.dev-dependencies] pytest = "^7.1"