From 03049a01f3c154e20a6cdabe008a8f106804c0b2 Mon Sep 17 00:00:00 2001 From: Yevhenii Vaskivskyi Date: Sun, 19 Nov 2023 22:33:33 +0100 Subject: [PATCH] 1.1.0 (#379) * Add tests and improve code (#372) * Add tests and improve code * Fix errors * Update tools module (#373) * Improve readers module (#374) * Improve writers module (#375) * Remove custom logger module (#376) * Add some tests and code improvements (#377) * Add RT-AX88U / Merlin test data (#378) * Bump version to `1.1.0` --- .coveragerc | 5 + .github/workflows/ci.yml | 64 + .github/workflows/release.yml | 24 +- .gitignore | 4 +- asusrouter/__main__.py | 8 + asusrouter/asusrouter.py | 2 - asusrouter/const.py | 6 +- asusrouter/modules/connection.py | 5 +- asusrouter/modules/endpoint/__init__.py | 3 +- asusrouter/modules/endpoint/command.py | 2 +- asusrouter/modules/endpoint/devicemap.py | 102 +- asusrouter/modules/endpoint/ethernet_ports.py | 2 +- asusrouter/modules/endpoint/onboarding.py | 10 +- asusrouter/tools/__init__.py | 10 - asusrouter/tools/converters.py | 239 ++- asusrouter/tools/logger.py | 36 - asusrouter/tools/readers.py | 25 +- asusrouter/tools/writers.py | 6 +- codecov.yml | 20 + pyproject.toml | 7 +- requirements_test.txt | 4 + tests/__init__.py | 2 +- tests/modules/__init__.py | 1 + tests/modules/endpoint/__init__.py | 1 + tests/modules/endpoint/test_command.py | 27 + tests/modules/endpoint/test_devicemap.py | 345 ++++ tests/modules/endpoint/test_endpoint.py | 183 ++ tests/test_data/__init__.py | 1 + .../test_data/rt_ax88u_merlin_388/__init__.py | 1 + .../rt_ax88u_merlin_388/devicemap_001.content | 86 + .../rt_ax88u_merlin_388/devicemap_001.py | 114 ++ .../ethernet_ports_001.content | 2 + .../rt_ax88u_merlin_388/ethernet_ports_001.py | 49 + .../rt_ax88u_merlin_388/firmware_001.content | 25 + .../rt_ax88u_merlin_388/firmware_001.py | 26 + .../rt_ax88u_merlin_388/hook_001.content | 5 + .../test_data/rt_ax88u_merlin_388/hook_001.py | 28 + .../rt_ax88u_merlin_388/hook_002.content | 3 + .../test_data/rt_ax88u_merlin_388/hook_002.py | 6 + .../rt_ax88u_merlin_388/hook_003.content | 7 + .../test_data/rt_ax88u_merlin_388/hook_003.py | 60 + .../rt_ax88u_merlin_388/hook_004.content | 4 + .../test_data/rt_ax88u_merlin_388/hook_004.py | 6 + .../rt_ax88u_merlin_388/hook_005.content | 63 + .../test_data/rt_ax88u_merlin_388/hook_005.py | 61 + .../rt_ax88u_merlin_388/hook_006.content | 48 + .../test_data/rt_ax88u_merlin_388/hook_006.py | 74 + .../rt_ax88u_merlin_388/hook_007.content | 64 + .../test_data/rt_ax88u_merlin_388/hook_007.py | 72 + .../onboarding_001.content | 9 + .../rt_ax88u_merlin_388/onboarding_001.py | 197 +++ .../port_status_001.content | 1 + .../rt_ax88u_merlin_388/port_status_001.py | 162 ++ .../rt_ax88u_merlin_388/sysinfo_001.content | 10 + .../rt_ax88u_merlin_388/sysinfo_001.py | 44 + .../temperature_001.content | 11 + .../rt_ax88u_merlin_388/temperature_001.py | 12 + .../update_clients_001.content | 8 + .../rt_ax88u_merlin_388/update_clients_001.py | 1560 +++++++++++++++++ .../rt_ax88u_merlin_388/vpn_001.content | 21 + .../test_data/rt_ax88u_merlin_388/vpn_001.py | 18 + tests/test_devices.py | 151 ++ tests/tools/__init__.py | 1 + tests/tools/test_cleaners.py | 65 + tests/tools/test_converters.py | 489 ++++++ tests/tools/test_readers.py | 168 ++ tests/tools/test_writers.py | 19 + tests/util/__init__.py | 1 - tests/util/test_calculators.py | 73 - tests/util/test_converters.py | 106 -- 70 files changed, 4687 insertions(+), 387 deletions(-) create mode 100644 .coveragerc create mode 100644 .github/workflows/ci.yml delete mode 100644 asusrouter/tools/logger.py create mode 100644 codecov.yml create mode 100644 requirements_test.txt create mode 100644 tests/modules/__init__.py create mode 100644 tests/modules/endpoint/__init__.py create mode 100644 tests/modules/endpoint/test_command.py create mode 100644 tests/modules/endpoint/test_devicemap.py create mode 100644 tests/modules/endpoint/test_endpoint.py create mode 100644 tests/test_data/__init__.py create mode 100644 tests/test_data/rt_ax88u_merlin_388/__init__.py create mode 100644 tests/test_data/rt_ax88u_merlin_388/devicemap_001.content create mode 100644 tests/test_data/rt_ax88u_merlin_388/devicemap_001.py create mode 100644 tests/test_data/rt_ax88u_merlin_388/ethernet_ports_001.content create mode 100644 tests/test_data/rt_ax88u_merlin_388/ethernet_ports_001.py create mode 100644 tests/test_data/rt_ax88u_merlin_388/firmware_001.content create mode 100644 tests/test_data/rt_ax88u_merlin_388/firmware_001.py create mode 100644 tests/test_data/rt_ax88u_merlin_388/hook_001.content create mode 100644 tests/test_data/rt_ax88u_merlin_388/hook_001.py create mode 100644 tests/test_data/rt_ax88u_merlin_388/hook_002.content create mode 100644 tests/test_data/rt_ax88u_merlin_388/hook_002.py create mode 100644 tests/test_data/rt_ax88u_merlin_388/hook_003.content create mode 100644 tests/test_data/rt_ax88u_merlin_388/hook_003.py create mode 100644 tests/test_data/rt_ax88u_merlin_388/hook_004.content create mode 100644 tests/test_data/rt_ax88u_merlin_388/hook_004.py create mode 100644 tests/test_data/rt_ax88u_merlin_388/hook_005.content create mode 100644 tests/test_data/rt_ax88u_merlin_388/hook_005.py create mode 100644 tests/test_data/rt_ax88u_merlin_388/hook_006.content create mode 100644 tests/test_data/rt_ax88u_merlin_388/hook_006.py create mode 100644 tests/test_data/rt_ax88u_merlin_388/hook_007.content create mode 100644 tests/test_data/rt_ax88u_merlin_388/hook_007.py create mode 100644 tests/test_data/rt_ax88u_merlin_388/onboarding_001.content create mode 100644 tests/test_data/rt_ax88u_merlin_388/onboarding_001.py create mode 100644 tests/test_data/rt_ax88u_merlin_388/port_status_001.content create mode 100644 tests/test_data/rt_ax88u_merlin_388/port_status_001.py create mode 100644 tests/test_data/rt_ax88u_merlin_388/sysinfo_001.content create mode 100644 tests/test_data/rt_ax88u_merlin_388/sysinfo_001.py create mode 100644 tests/test_data/rt_ax88u_merlin_388/temperature_001.content create mode 100644 tests/test_data/rt_ax88u_merlin_388/temperature_001.py create mode 100644 tests/test_data/rt_ax88u_merlin_388/update_clients_001.content create mode 100644 tests/test_data/rt_ax88u_merlin_388/update_clients_001.py create mode 100644 tests/test_data/rt_ax88u_merlin_388/vpn_001.content create mode 100644 tests/test_data/rt_ax88u_merlin_388/vpn_001.py create mode 100644 tests/test_devices.py create mode 100644 tests/tools/__init__.py create mode 100644 tests/tools/test_cleaners.py create mode 100644 tests/tools/test_converters.py create mode 100644 tests/tools/test_readers.py create mode 100644 tests/tools/test_writers.py delete mode 100644 tests/util/__init__.py delete mode 100644 tests/util/test_calculators.py delete mode 100644 tests/util/test_converters.py diff --git a/.coveragerc b/.coveragerc new file mode 100644 index 0000000..69b9a42 --- /dev/null +++ b/.coveragerc @@ -0,0 +1,5 @@ +[run] +source = asusrouter +omit = + # Constants modules + asusrouter/modules/endpoint/devicemap_const.py diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..f4f33c3 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,64 @@ +name: CI + +on: + push: + branches: + - main + - dev + pull_request: ~ + schedule: + - cron: '0 0 * * *' + +env: + DEFAULT_PYTHON: 3.11 + +concurrency: + group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: true + +jobs: + test: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4.1.1 + + - name: Set up Python ${{ env.DEFAULT_PYTHON }} + uses: actions/setup-python@v4.7.1 + with: + python-version: ${{ env.DEFAULT_PYTHON }} + + - name: Get pip cache directory path + id: pip-cache + run: echo "PIP_CACHE_DIR=$(pip cache dir)" >> $GITHUB_ENV + + - name: Cache pip dependencies + uses: actions/cache@v3.3.2 + with: + path: ${{ env.PIP_CACHE_DIR }} + key: ${{ runner.os }}-pip-${{ hashFiles('**/pyproject.toml') }} + restore-keys: | + ${{ runner.os }}-pip- + + - name: Install project dependencies + run: | + pip install . + + - name: Install test dependencies + run: | + pip install -r requirements_test.txt + + - name: Run unit tests + run: | + pytest --cov=asusrouter --cov-report=xml:unit-tests-cov.xml -k 'not test_devices' + + - name: Run real-data tests + run: | + pytest --cov=asusrouter --cov-report=xml:real-data-tests-cov.xml tests/test_devices.py --log-cli-level=INFO + + - name: Upload coverage to Codecov + uses: codecov/codecov-action@v3.1.4 + with: + token: ${{ secrets.CODECOV_TOKEN }} + fail_ci_if_error: true + files: unit-tests-cov.xml,real-data-tests-cov.xml diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 023ec93..f490cec 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -23,16 +23,28 @@ jobs: with: python-version: ${{ env.DEFAULT_PYTHON }} - - name: Build package - shell: bash + - name: Cache pip dependencies + uses: actions/cache@v3 + with: + path: ~/.cache/pip + key: ${{ runner.os }}-pip-${{ hashFiles('**/requirements.txt') }} + restore-keys: | + ${{ runner.os }}-pip- + + - name: Install dependencies run: | + pip install -r requirements.txt + pip install pytest pip install build twine - python -m build + + - name: Run tests + run: pytest + + - name: Build package + run: python -m build - name: Publish package - shell: bash run: | export TWINE_USERNAME="__token__" export TWINE_PASSWORD="${{ secrets.TWINE_TOKEN }}" - - twine upload dist/* --skip-existing + twine upload dist/* --skip-existing || exit 1 diff --git a/.gitignore b/.gitignore index 6d8d746..d90483f 100644 --- a/.gitignore +++ b/.gitignore @@ -5,10 +5,12 @@ __pycache__/ dist/ *.egg-info +# Pytest +.coverage ### Other # Test -test*.py +local_test*.py local/ # VS Code diff --git a/asusrouter/__main__.py b/asusrouter/__main__.py index 183bb4d..0acfc4b 100644 --- a/asusrouter/__main__.py +++ b/asusrouter/__main__.py @@ -9,6 +9,7 @@ import aiohttp from asusrouter import AsusRouter, AsusRouterDump, AsusRouterError +from asusrouter.modules.data import AsusData _LOGGER = logging.getLogger(__name__) @@ -43,6 +44,13 @@ async def _connect_and_dump(args: argparse.Namespace) -> None: _LOGGER.debug("Connected and identified") + _LOGGER.debug("Checking all known data...") + + for datatype in AsusData: + await router.async_get_data(datatype) + + _LOGGER.debug("Finished checking all known data") + # Disconnect from the router await router.async_disconnect() _LOGGER.debug("Disconnected") diff --git a/asusrouter/asusrouter.py b/asusrouter/asusrouter.py index 9f89ecf..9a869c0 100644 --- a/asusrouter/asusrouter.py +++ b/asusrouter/asusrouter.py @@ -269,8 +269,6 @@ async def _check_flags(self) -> None: ): flags = state_flags.data - print("FLAGS: ", flags) - if flags.get("reboot", False) is True: _LOGGER.debug("Reboot flag is set") await self._async_handle_reboot() diff --git a/asusrouter/const.py b/asusrouter/const.py index 3e4fe0f..8777f56 100644 --- a/asusrouter/const.py +++ b/asusrouter/const.py @@ -7,9 +7,13 @@ class ContentType(str, Enum): """Content type enum.""" + UNKNOWN = "unknown" + + BINARY = "application/octet-stream" + HTML = "text/html" JSON = "application/json" TEXT = "text/plain" - HTML = "text/html" + XML = "application/xml" # Asus constants diff --git a/asusrouter/modules/connection.py b/asusrouter/modules/connection.py index 42c67ec..1ad5c7d 100644 --- a/asusrouter/modules/connection.py +++ b/asusrouter/modules/connection.py @@ -4,8 +4,7 @@ from typing import Optional from asusrouter.modules.wlan import Wlan -from asusrouter.tools import get_enum_key_by_value -from asusrouter.tools.converters import safe_int +from asusrouter.tools.converters import get_enum_key_by_value, safe_int class ConnectionState(IntEnum): @@ -69,4 +68,4 @@ def get_connection_type(value: Optional[int]) -> ConnectionType: # Check that it's actually an int value = safe_int(value) or 0 - return CONNECTION_TYPE.get(value, ConnectionType.WIRED) + return get_enum_key_by_value(ConnectionType, value, ConnectionType.WIRED) diff --git a/asusrouter/modules/endpoint/__init__.py b/asusrouter/modules/endpoint/__init__.py index fa4440b..1693e94 100644 --- a/asusrouter/modules/endpoint/__init__.py +++ b/asusrouter/modules/endpoint/__init__.py @@ -11,7 +11,6 @@ from asusrouter.error import AsusRouter404Error from asusrouter.modules.data import AsusData, AsusDataState from asusrouter.modules.firmware import Firmware -from asusrouter.modules.flags import Flag from asusrouter.modules.wlan import Wlan _LOGGER = logging.getLogger(__name__) @@ -126,7 +125,7 @@ def data_set(data: dict[str, Any], **kwargs: Any) -> dict[str, Any]: return data -def data_get(data: dict[str, Any], key: str) -> Any: +def data_get(data: dict[str, Any], key: str) -> Optional[Any]: """Extract value from the data dict and update the data dict.""" # Get the value diff --git a/asusrouter/modules/endpoint/command.py b/asusrouter/modules/endpoint/command.py index 8013fb9..9e2dd06 100644 --- a/asusrouter/modules/endpoint/command.py +++ b/asusrouter/modules/endpoint/command.py @@ -7,7 +7,7 @@ from asusrouter.tools.readers import read_json_content -def read(content: str) -> dict[str, Any]: # pylint: disable=unused-argument +def read(content: str) -> dict[str, Any]: """Read state data""" # Read the json content diff --git a/asusrouter/modules/endpoint/devicemap.py b/asusrouter/modules/endpoint/devicemap.py index 2bc6ee4..23df0dd 100644 --- a/asusrouter/modules/endpoint/devicemap.py +++ b/asusrouter/modules/endpoint/devicemap.py @@ -2,6 +2,7 @@ from __future__ import annotations +import logging import re from datetime import datetime, timedelta from typing import Any, Optional, Tuple @@ -18,6 +19,8 @@ from .devicemap_const import DEVICEMAP_BY_INDEX, DEVICEMAP_BY_KEY, DEVICEMAP_CLEAR +_LOGGER = logging.getLogger(__name__) + REQUIRE_HISTORY = True @@ -28,8 +31,13 @@ def read(content: str) -> dict[str, Any]: devicemap: dict[str, Any] = {} # Parse the XML data - xml_content: dict[str, Any] = xmltodict.parse(content).get("devicemap", {}) - if not xml_content: + try: + xml_content: dict[str, Any] = xmltodict.parse(content).get("devicemap", {}) + if not xml_content: + _LOGGER.debug("Received empty devicemap XML") + return devicemap + except xmltodict.expat.ExpatError as ex: # type: ignore + _LOGGER.debug("Received invalid devicemap XML: %s", ex) return devicemap # Go through the data and fill the dict @@ -42,6 +50,8 @@ def read(content: str) -> dict[str, Any]: # Clear values from useless symbols for output_group, clear_map in DEVICEMAP_CLEAR.items(): + if output_group not in devicemap: + continue for key, clear_value in clear_map.items(): # If the key is not in the devicemap, continue if key not in devicemap[output_group]: @@ -61,22 +71,30 @@ def read(content: str) -> dict[str, Any]: return devicemap -# This method performs reading of the devicemap by index -# to simplify the original read_devicemap method def read_index(xml_content: dict[str, Any]) -> dict[str, Any]: - """Read devicemap by index.""" + """Read devicemap by index. + + This method performs reading of the devicemap by index + to simplify the original read_devicemap method.""" # Create a dict to store the data devicemap: dict[str, Any] = {} # Get values for which we only know their order (index) for output_group, input_group, input_values in DEVICEMAP_BY_INDEX: - # Create a dict to store the data - output_group_data: dict[str, Any] = {} + # Create an empty dictionary for the output group + devicemap[output_group] = {} - # Go through the input values and fill the dict - for index, input_value in enumerate(input_values): - output_group_data[input_value] = xml_content[input_group][index] + # Check that the input group is in the xml content + if input_group not in xml_content: + continue + + # Use dict comprehension to build output_group_data + output_group_data = { + input_value: xml_content[input_group][index] + for index, input_value in enumerate(input_values) + if index < len(xml_content[input_group]) + } # Add the output group data to the devicemap devicemap[output_group] = output_group_data @@ -85,10 +103,11 @@ def read_index(xml_content: dict[str, Any]) -> dict[str, Any]: return devicemap -# This method performs reading of the devicemap by key -# to simplify the original read_devicemap method def read_key(xml_content: dict[str, Any]) -> dict[str, Any]: - """Read devicemap by key.""" + """Read devicemap by key. + + This method performs reading of the devicemap by key + to simplify the original read_devicemap method.""" # Create a dict to store the data devicemap: dict[str, Any] = {} @@ -100,27 +119,25 @@ def read_key(xml_content: dict[str, Any]) -> dict[str, Any]: # Go through the input values and fill the dict for input_value in input_values: - # Check if the input group is a string - if isinstance(xml_content.get(input_group), str): - # Check if the input value is in the input group - if input_value in xml_content[input_group]: - # Add the input value to the output group data and remove the key - output_group_data[input_value] = xml_content[input_group].replace( + # Get the input group data + xml_input_group = xml_content.get(input_group) + + # If the input group data is None, skip this iteration + if xml_input_group is None: + continue + + # If the input group data is a string, convert it to a list + if isinstance(xml_input_group, str): + xml_input_group = [xml_input_group] + + # Go through the input group data and check if the input value is in it + for value in xml_input_group: + if input_value in value: + # Add the input value to the output group data + output_group_data[input_value] = value.replace( f"{input_value}=", "" ) - # Check if the input group is a list - else: - # Go through the input group and check if the input value is in it - xml_input_group = xml_content.get(input_group) - if not xml_input_group: - continue - for value in xml_input_group: - if input_value in value: - # Add the input value to the output group data - output_group_data[input_value] = value.replace( - f"{input_value}=", "" - ) - break + break # Add the output group data to the devicemap devicemap[output_group] = output_group_data @@ -142,17 +159,24 @@ def read_special(xml_content: dict[str, Any]) -> dict[str, Any]: def read_uptime_string(content: str) -> datetime | None: """Read uptime string and return proper datetime object.""" + # Split the content into the date/time part and the seconds part + uptime_parts = content.split("(") + if len(uptime_parts) < 2: + return None + + # Extract the number of seconds from the seconds part + seconds_match = re.search("([0-9]+)", uptime_parts[1]) + if not seconds_match: + return None + try: - part = content.split("(") - match = re.search("([0-9]+)", part[1]) - if not match: - return None - seconds = int(match.group()) - when = dtparse(part[0]) - uptime = when - timedelta(seconds=seconds) + seconds = int(seconds_match.group()) + when = dtparse(uptime_parts[0]) except ValueError: return None + uptime = when - timedelta(seconds=seconds) + return uptime diff --git a/asusrouter/modules/endpoint/ethernet_ports.py b/asusrouter/modules/endpoint/ethernet_ports.py index 9fbfb3c..50fc5e2 100644 --- a/asusrouter/modules/endpoint/ethernet_ports.py +++ b/asusrouter/modules/endpoint/ethernet_ports.py @@ -25,7 +25,7 @@ def process(data: dict[str, Any]) -> dict[AsusData, Any]: """Process ethernet ports data.""" # Ports info - ports = { + ports: dict[PortType, dict] = { PortType.LAN: {}, PortType.WAN: {}, } diff --git a/asusrouter/modules/endpoint/onboarding.py b/asusrouter/modules/endpoint/onboarding.py index b89c353..b405985 100644 --- a/asusrouter/modules/endpoint/onboarding.py +++ b/asusrouter/modules/endpoint/onboarding.py @@ -8,7 +8,7 @@ from asusrouter.modules.aimesh import AiMeshDevice from asusrouter.modules.data import AsusData from asusrouter.tools.cleaners import clean_content -from asusrouter.tools.converters import safe_bool, safe_int, safe_none_or_str +from asusrouter.tools.converters import safe_bool, safe_int, safe_return from asusrouter.tools.readers import read_json_content CONNECTION_TYPE = { @@ -63,7 +63,7 @@ def process(data: dict[str, Any]) -> dict[AsusData, Any]: description = { "connection_type": convert["connection_type"], "guest": convert["guest"], - "ip": safe_none_or_str( + "ip": safe_return( client_list[node][connection][mac].get("ip", None) ), "mac": mac, @@ -105,8 +105,8 @@ def process_aimesh_node(data: dict[str, Any]) -> AiMeshDevice: if f"pap{el}" in data and data[f"pap{el}"] is not str(): parent["connection"] = const_ap[el] parent["mac"] = data[f"pap{el}"] - parent["rssi"] = safe_none_or_str(data.get(f"rssi{el}")) - parent["ssid"] = safe_none_or_str(data.get(f"pap{el}_ssid")) + parent["rssi"] = safe_return(data.get(f"rssi{el}")) + parent["ssid"] = safe_return(data.get(f"pap{el}_ssid")) level = safe_int(data.get("level", "0")) node_type = "router" if level == 0 else "node" @@ -118,7 +118,7 @@ def process_aimesh_node(data: dict[str, Any]) -> AiMeshDevice: product_id=data.get("product_id"), ip=data.get("ip"), fw=data.get("fwver", None), - fw_new=safe_none_or_str(data.get("newfwver")), + fw_new=safe_return(data.get("newfwver")), mac=data.get("mac", None), ap=ap, parent=parent, diff --git a/asusrouter/tools/__init__.py b/asusrouter/tools/__init__.py index 95c10fb..6035c70 100644 --- a/asusrouter/tools/__init__.py +++ b/asusrouter/tools/__init__.py @@ -1,11 +1 @@ """Tools module""" - - -def get_enum_key_by_value(enum, value, default=None): - """Get the enum key by value""" - - for key, enum_value in enum.__members__.items(): - if enum_value.value == value: - return enum[key] - - return default diff --git a/asusrouter/tools/converters.py b/asusrouter/tools/converters.py index 4cfefe0..95758c6 100644 --- a/asusrouter/tools/converters.py +++ b/asusrouter/tools/converters.py @@ -11,10 +11,28 @@ from datetime import datetime, timedelta, timezone from enum import Enum -from typing import Any, Callable, Iterable, Optional, Tuple +from typing import Any, Callable, Iterable, Optional, Type, TypeVar, cast from dateutil.parser import parse as dtparse +true_values = {"true", "allow", "1", "on", "enabled"} +false_values = {"false", "block", "0", "off", "disabled"} + + +_T = TypeVar("_T") +_E = TypeVar("_E", bound=Enum) + + +def clean_input(func: Callable[..., Any]) -> Callable[..., Any]: + """Decorator to clean input data.""" + + def wrapper(content: Any, *args, **kwargs) -> Any: + if isinstance(content, str): + return func(clean_string(content), *args, **kwargs) + return func(content, *args, **kwargs) + + return wrapper + def clean_string(content: Optional[str]) -> Optional[str]: """Get a clean string or return None if it is empty.""" @@ -46,7 +64,7 @@ def flatten_dict( return {} items = [] - exclude = tuple(exclude or []) + exclude = (exclude,) if isinstance(exclude, str) else tuple(exclude or []) for k, v in d.items(): new_key = f"{parent_key}{sep}{k}" if parent_key not in ("", None) else k # We have a dict - check it @@ -65,13 +83,45 @@ def flatten_dict( return dict(items) -def is_enum(v): +def get_enum_key_by_value( + enum: Type[_E], value: Any, default: Optional[_E] = None +) -> _E: + """Get the enum key by value""" + + if issubclass(enum, Enum): + for enum_value in enum: + if enum_value.value == value: + return enum_value + + if default is not None: + return default + + raise ValueError(f"Invalid value: {value}") + + +def handle_none_content(content: Optional[_T], default: Optional[_T]) -> Optional[_T]: + """Return the default value if content is None, else return the content.""" + + if content is None: + return default + return content + + +def is_enum(v) -> bool: + """Check if the value is an enum.""" + return isinstance(v, type) and issubclass(v, Enum) -def list_from_dict(raw: dict[str, Any]) -> list[str]: +def list_from_dict(raw: Optional[dict[Any, Any] | list[Any]]) -> list[str]: """Return dictionary keys as list.""" + if isinstance(raw, list): + return raw + + if not isinstance(raw, dict): + return [] + return list(raw.keys()) @@ -81,6 +131,9 @@ def nvram_get(content: Optional[list[str] | str]) -> Optional[list[tuple[str, .. if not content: return None + if not isinstance(content, (list, str)): + content = str(content) + if isinstance(content, str): content = [content] @@ -110,85 +163,94 @@ def run_method( return value +@clean_input def safe_bool(content: Optional[str | int | float | bool]) -> Optional[bool]: """Read the content as boolean or return None.""" - result = None + if content is None: + return None + + if isinstance(content, bool): + return content + if isinstance(content, (int, float)): + return content != 0 + if isinstance(content, str): + content = content.lower() + if content in true_values: + return True + if content in false_values: + return False - if content: - if isinstance(content, bool): - result = content - elif isinstance(content, (int, float)): - result = content != 0 - else: - content = clean_string(content) - if content: - content = content.lower() - if content in ("true", "allow", "1", "on", "enabled"): - result = True - elif content in ("false", "block", "0", "off", "disabled"): - result = False + return None - return result +def safe_convert( + convert_func: Callable[[str | int | float], _T], + content: Optional[str | int | float], + default: Optional[_T] = None, + fallback_func: Optional[Callable[[Any], _T]] = None, +) -> Optional[_T]: + """Try to convert the content using the conversion function, + return the default value if it fails.""" + if content is None: + return default + try: + return convert_func(content) + except ValueError: + if fallback_func is not None and isinstance(content, str): + try: + return fallback_func(content) + except ValueError: + pass + return default + + +@clean_input def safe_datetime(content: Optional[str]) -> Optional[datetime]: """Read the content as datetime or return None.""" - content = clean_string(content) if not content: return None try: return dtparse(content) - except Exception: # pylint: disable=broad-except + except (ValueError, TypeError): return None +@clean_input def safe_exists(content: Optional[str]) -> bool: """Read the content as boolean or return None.""" - content = clean_string(content) - if not content: + if content is None: return False return True +@clean_input def safe_float( content: Optional[str | int | float], default: Optional[float] = None ) -> Optional[float]: """Read the content as float or return None.""" - if isinstance(content, (int, float)): - return float(content) - - content = clean_string(content) - if not content: - return default - - try: - return float(content) - except ValueError: - return default + content = cast(Optional[str | int | float], handle_none_content(content, default)) + return safe_convert(float, content, default) +@clean_input def safe_int( - content: Optional[str | int], default: Optional[int] = None, base: int = 10 + content: Optional[str | int | float], default: Optional[int] = None, base: int = 10 ) -> Optional[int]: """Read the content as int or return the default value (None if not specified).""" - if isinstance(content, int): - return content - - content = clean_string(content) - if not content: - return default if isinstance(default, int) else None - - try: - return int(content, base=base) - except ValueError: - return default if isinstance(default, int) else None + content = cast(Optional[str | int | float], handle_none_content(content, default)) + if isinstance(content, str): + return safe_convert( + lambda x: int(x, base=base), content, default, lambda x: int(float(x)) + ) + return safe_convert(int, content, default if isinstance(default, int) else None) def safe_list(content: Any) -> list[Any]: @@ -209,35 +271,20 @@ def safe_list_csv(content: Optional[str]) -> list[str]: return safe_list_from_string(content, ",") +@clean_input def safe_list_from_string(content: Optional[str], delimiter: str = " ") -> list[str]: """Read the content as list or return empty list.""" - content = clean_string(content) - if not content: + if not isinstance(content, str): return [] return content.split(delimiter) -def safe_none_or_str(content: Optional[str]) -> Optional[str]: - """Read the content as string or return None if it is empty.""" - - if content is None or not isinstance(content, str): - return None - - content = content.strip() - if content == str(): - return None - - return content - - +@clean_input def safe_return(content: Any) -> Any: """Return the content.""" - if isinstance(content, str): - return clean_string(content) - return content @@ -261,16 +308,16 @@ def safe_speed( def safe_time_from_delta(content: str) -> datetime: """Transform time delta to the date in the past.""" - return datetime.utcnow().replace( + return datetime.now(timezone.utc).replace( microsecond=0, tzinfo=timezone.utc ) - safe_timedelta_long(content) +@clean_input def safe_timedelta_long(content: Optional[str]) -> timedelta: """Transform connection timedelta of the device to a proper datetime object when the device was connected""" - content = clean_string(content) if not content: return timedelta() @@ -279,27 +326,54 @@ def safe_timedelta_long(content: Optional[str]) -> timedelta: return timedelta( hours=int(part[-3]), minutes=int(part[-2]), seconds=int(part[-1]) ) - except ValueError: + except (ValueError, IndexError): return timedelta() def safe_unpack_key( - content: tuple[str, Optional[Callable[..., Any]] | list[Callable[..., Any]]] | str -) -> tuple[str, Optional[Callable[..., Any]]]: - """Method to unpack key/method tuple - even if some values are missing.""" + content: tuple[str, Optional[Callable[..., Any]] | list[Callable[..., Any]]] + | str + | tuple[str] +) -> tuple[str, Optional[Callable[..., Any] | list[Callable[..., Any]]]]: + """ + Unpacks a tuple containing a key and a method. + + The input can be a tuple of a string and a method, a single string, + or a single-item tuple with a string. + If the input is a string or a single-item tuple, the returned method is None. + If the input is a tuple of a string and a method, both are returned as is. + + Args: + content: A tuple of a string and a method, a single string, + or a single-item tuple with a string. + + Returns: + A tuple containing a string and a method. + If no method was provided in the input, None is returned as the method. + """ if isinstance(content, tuple): - # All 2 values are present - if len(content) == 2: - return content + key = content[0] + if len(content) > 1: + content = cast( + tuple[str, Optional[Callable[..., Any]] | list[Callable[..., Any]]], + content, + ) + methods = content[1] + if methods is not None and not ( + callable(methods) or isinstance(methods, Iterable) + ): + methods = None + else: + methods = None + return key, methods # No method selected - return (content, None) + return content, None def safe_unpack_keys( - content: tuple[str, str, Callable[..., Any] | list[Callable[..., Any]]] + content: tuple[str, str, Optional[Callable[..., Any]] | list[Callable[..., Any]]] | tuple[str, str] | str ) -> tuple[Any, ...]: @@ -317,8 +391,8 @@ def safe_unpack_keys( # No method and key_to_use selected # We need to replace key_to_use with key - content = (content, content) - return content + (None,) + new_content = (content, content) + return new_content + (None,) def safe_usage(used: int | float, total: int | float) -> float: @@ -348,4 +422,11 @@ def safe_usage_historic( This method is just an interface to calculate usage using `usage` method""" - return safe_usage(used - prev_used, total - prev_total) + used_diff = used - prev_used + total_diff = total - prev_total + + # Don't allow negative differences + if used_diff < 0 or total_diff < 0: + return 0.0 + + return safe_usage(used_diff, total_diff) diff --git a/asusrouter/tools/logger.py b/asusrouter/tools/logger.py deleted file mode 100644 index 170f916..0000000 --- a/asusrouter/tools/logger.py +++ /dev/null @@ -1,36 +0,0 @@ -"""Logger module for AsusRouter. - -This module is needed to add a TRACE logging level to the logger, -which provides more information than DEBUG. In general, this information -is not needed for general use, but can help in tracing issues. - -This module also adds an UNSAFE logging level, which can provide sensitive -information, including passwords. Not recommended for general use.""" - -from __future__ import annotations - -import logging - -TRACE = 5 -UNSAFE = 3 - -logging.addLevelName(TRACE, "TRACE") -logging.addLevelName(UNSAFE, "UNSAFE") - - -def trace(self, message, *args, **kws): - """Trace logging level.""" - - if self.isEnabledFor(TRACE): - self._log(TRACE, message, args, **kws) # pylint: disable=protected-access - - -def unsafe(self, message, *args, **kws): - """Unsafe logging level.""" - - if self.isEnabledFor(UNSAFE): - self._log(UNSAFE, message, args, **kws) # pylint: disable=protected-access - - -logging.Logger.trace = trace -logging.Logger.unsafe = unsafe diff --git a/asusrouter/tools/readers.py b/asusrouter/tools/readers.py index df97b9d..14d8332 100644 --- a/asusrouter/tools/readers.py +++ b/asusrouter/tools/readers.py @@ -8,7 +8,7 @@ from typing import Any, Optional from asusrouter.const import ContentType -from asusrouter.tools.converters import clean_string +from asusrouter.tools.converters import clean_input _LOGGER = logging.getLogger(__name__) @@ -92,7 +92,7 @@ def read_content_type(headers: dict[str, str]) -> ContentType: return content_type_enum # If the content type is not found, return the content type as text - return ContentType.TEXT + return ContentType.UNKNOWN def read_js_variables(content: str) -> dict[str, Any]: @@ -127,11 +127,10 @@ def read_js_variables(content: str) -> dict[str, Any]: return js_variables +@clean_input def read_json_content(content: Optional[str]) -> dict[str, Any]: """Get the json content""" - content = clean_string(content) - if not content: return {} @@ -142,7 +141,7 @@ def read_json_content(content: Optional[str]) -> dict[str, Any]: # Return the json content try: return json.loads(content.encode().decode("utf-8-sig")) - except (json.JSONDecodeError, UnicodeDecodeError) as ex: + except json.JSONDecodeError as ex: _LOGGER.error( "Unable to decode json content with exception `%s`. Please, copy this end fill in a bug report: %s", ex, @@ -151,18 +150,12 @@ def read_json_content(content: Optional[str]) -> dict[str, Any]: return {} -def readable_mac(raw: str) -> bool: +@clean_input +def readable_mac(raw: Optional[str]) -> bool: """Checks if string is MAC address""" - if not isinstance(raw, str): - return False - - raw = raw.strip() - - if raw == str(): - return False - - if re.search(re.compile("^([0-9A-Fa-f]{2}[:-]){5}([0-9A-Fa-f]{2})"), raw): - return True + if isinstance(raw, str): + if re.search(re.compile("^([0-9A-Fa-f]{2}[:-]){5}([0-9A-Fa-f]{2})$"), raw): + return True return False diff --git a/asusrouter/tools/writers.py b/asusrouter/tools/writers.py index 7012230..238ef66 100644 --- a/asusrouter/tools/writers.py +++ b/asusrouter/tools/writers.py @@ -5,16 +5,16 @@ from __future__ import annotations +from asusrouter.tools.converters import clean_input + +@clean_input def nvram(content: str | list[str] | None = None) -> str | None: """NVRAM writer. This function converts a list of strings (or a single string) into a string request to the NVRAM read endpoint.""" - if not content: - return None - if isinstance(content, str): return f"nvram_get({content});" diff --git a/codecov.yml b/codecov.yml new file mode 100644 index 0000000..e6be5ea --- /dev/null +++ b/codecov.yml @@ -0,0 +1,20 @@ +codecov: + require_ci_to_pass: yes + +coverage: + precision: 2 + round: down + range: "0...100" + +comment: + layout: "reach, diff, flags, files" + behavior: default + require_changes: no + +status: + project: + default: + threshold: 1% + patch: + default: + threshold: 1% diff --git a/pyproject.toml b/pyproject.toml index 41ee4f9..b937aa7 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "asusrouter" -version = "1.1.0b2" +version = "1.1.0" license = {text = "Apache-2.0"} requires-python = ">=3.11.0" readme = "README.md" @@ -33,3 +33,8 @@ dependencies = [ [tool.setuptools.packages.find] include = ["asusrouter*"] + +[tool.pytest.ini_options] +testpaths = [ + "tests", +] diff --git a/requirements_test.txt b/requirements_test.txt new file mode 100644 index 0000000..032a155 --- /dev/null +++ b/requirements_test.txt @@ -0,0 +1,4 @@ +# Pytest for running tests +pytest>=7.4.3 +pytest-asyncio>=0.21.1 +pytest-cov>=4.1.0 diff --git a/tests/__init__.py b/tests/__init__.py index a8991d4..2d555c0 100644 --- a/tests/__init__.py +++ b/tests/__init__.py @@ -1 +1 @@ -"""Tests for AsusRouter""" +"""Tests for AsusRouter.""" diff --git a/tests/modules/__init__.py b/tests/modules/__init__.py new file mode 100644 index 0000000..f6f3f2d --- /dev/null +++ b/tests/modules/__init__.py @@ -0,0 +1 @@ +"""Tests for the AsusRouter modules.""" diff --git a/tests/modules/endpoint/__init__.py b/tests/modules/endpoint/__init__.py new file mode 100644 index 0000000..2ed07c9 --- /dev/null +++ b/tests/modules/endpoint/__init__.py @@ -0,0 +1 @@ +"""Tests for AsusRouter endpoint module.""" diff --git a/tests/modules/endpoint/test_command.py b/tests/modules/endpoint/test_command.py new file mode 100644 index 0000000..704112d --- /dev/null +++ b/tests/modules/endpoint/test_command.py @@ -0,0 +1,27 @@ +"""Test AsusRouter command endpoint module.""" + +from unittest.mock import patch + +from asusrouter.modules.endpoint import command + + +def test_read(): + """Test read function.""" + + # Test data + content = '{"key1": "value1", "key2": "value2"}' + expected_command = {"key1": "value1", "key2": "value2"} + + # Mock the read_json_content function + with patch( + "asusrouter.modules.endpoint.command.read_json_content", + return_value=expected_command, + ) as mock_read_json_content: + # Call the function + result = command.read(content) + + # Check the result + assert result == expected_command + + # Check that read_json_content was called with the correct argument + mock_read_json_content.assert_called_once_with(content) diff --git a/tests/modules/endpoint/test_devicemap.py b/tests/modules/endpoint/test_devicemap.py new file mode 100644 index 0000000..fa2af8b --- /dev/null +++ b/tests/modules/endpoint/test_devicemap.py @@ -0,0 +1,345 @@ +"""Test AsusRouter devicemap endpoint module.""" + +from datetime import datetime, timedelta, timezone +from unittest.mock import ANY, MagicMock, patch + +import pytest + +from asusrouter.modules.data import AsusData, AsusDataState +from asusrouter.modules.endpoint import devicemap + + +def generate_xml_content(groups): + """Generate XML content based on input parameters.""" + content = "\n" + for group, keys in groups.items(): + content += f" <{group}>\n" + for key, value in keys.items(): + content += f" <{key}>{value}\n" + content += f" \n" + content += "\n" + return content + + +@pytest.mark.parametrize( + "content", + [ + "", # Empty devicemap + "non-xml", # Invalid XML + ], +) +def test_read_invalid(content): + """Test read function with empty devicemap.""" + + assert devicemap.read(content) == {} + + +@pytest.fixture +def common_group(): + """Return the common test data intermediate.""" + + return { + "group1": {"key1": "value1"}, + "group2": {"key2": "value2"}, + "group3": {"key3": "value3_test"}, + } + + +@pytest.fixture +def common_test_data_result(): + """Return the common test data result.""" + + return { + "group1": {"key1": "value1"}, + "group2": {"key2": "value2"}, + "group3": {"key3": "value3"}, + } + + +@pytest.fixture +def mock_functions(): + """Return the mock functions.""" + + with patch( + "asusrouter.modules.endpoint.devicemap.read_index", + return_value={"group1": {"key1": "value1"}, "group3": {"key3": "value3_test"}}, + ) as mock_read_index, patch( + "asusrouter.modules.endpoint.devicemap.read_key", + return_value={"group2": {"key2": "value2"}}, + ) as mock_read_key, patch( + "asusrouter.modules.endpoint.devicemap.merge_dicts", + side_effect=lambda x, y: {**x, **y}, + ) as mock_merge_dicts, patch( + "asusrouter.modules.endpoint.devicemap.clean_dict", side_effect=lambda x: x + ) as mock_clean_dict, patch( + "asusrouter.modules.endpoint.devicemap.clean_dict_key_prefix", + side_effect=lambda x, _: x, + ) as mock_clean_dict_key_prefix, patch( + "asusrouter.modules.endpoint.devicemap.DEVICEMAP_CLEAR", + new={"group3": {"key3": "_test", "key4": "_test"}, "group4": {"key5": "_test"}}, + ) as mock_devicemap_clear: + yield { + "read_index": mock_read_index, + "read_key": mock_read_key, + "merge_dicts": mock_merge_dicts, + "clean_dict": mock_clean_dict, + "clean_dict_key_prefix": mock_clean_dict_key_prefix, + "devicemap_clear": mock_devicemap_clear, + } + + +def test_read_with_data( + mock_functions, # pylint: disable=redefined-outer-name + common_test_data_result, # pylint: disable=redefined-outer-name + common_group, # pylint: disable=redefined-outer-name +): + """Test read function.""" + + # Test data + content = generate_xml_content(common_group) + expected_devicemap = common_test_data_result + + # Call the function + result = devicemap.read(content) + + # Check the result + assert result == expected_devicemap + + # Check the calls to the mocked functions + mock_functions["read_index"].assert_called_with(common_group) + mock_functions["read_key"].assert_called_with(common_group) + assert mock_functions["merge_dicts"].call_count == 2 + mock_functions["clean_dict"].assert_called_with(expected_devicemap) + assert mock_functions["clean_dict_key_prefix"].call_count == 3 + + +@pytest.fixture +def const_devicemap(): + """Return the const devicemap.""" + + return [ + ("output_group1", "group1", ["input_value1"]), + ("output_group2", "group2", ["input_value3", "input_value4"]), + ("output_group3", "group3", ["input_value5"]), + ("output_group4", "group4", ["input_value6"]), + ("output_group5", "group5", ["input_value2"]), + ("output_group6", "group6", ["input_value7"]), + ] + + +@pytest.fixture +def const_devicemap_result(): + """Return the const devicemap result.""" + + return { + "output_group1": {"input_value1": "value1"}, + "output_group2": {"input_value3": "value3", "input_value4": "value4"}, + "output_group3": {"input_value5": "value5"}, + "output_group4": {"input_value6": "value6"}, + "output_group5": {}, + "output_group6": {}, + } + + +@pytest.fixture +def input_data(): + """Return the input data for the tests.""" + return { + "group1": ["value1"], + "group2": ["value3", "value4"], + "group3": ["value5"], + "group4": ["value6"], + # "group5": [], + "group6": [], + } + + +@pytest.fixture +def input_data_key(): + """Return the input data for the read_key test.""" + return { + "group1": ["input_value1=value1"], + "group2": ["input_value3=value3", "input_value4=value4"], + "group3": "input_value5=value5", + "group4": "input_value6=value6", + "group5": None, + "group6": [], + } + + +def test_read_index( + const_devicemap, # pylint: disable=redefined-outer-name + const_devicemap_result, # pylint: disable=redefined-outer-name + input_data, # pylint: disable=redefined-outer-name +): + """Test read_index function.""" + with patch.object(devicemap, "DEVICEMAP_BY_INDEX", new=const_devicemap): + # Call the function + result = devicemap.read_index(input_data) + + # Check the result + assert result == const_devicemap_result + + +def test_read_key( + const_devicemap, # pylint: disable=redefined-outer-name + const_devicemap_result, # pylint: disable=redefined-outer-name + input_data_key, # pylint: disable=redefined-outer-name +): + """Test read_key function.""" + with patch.object(devicemap, "DEVICEMAP_BY_KEY", new=const_devicemap): + # Call the function + result = devicemap.read_key(input_data_key) + + # Check the result + assert result == const_devicemap_result + + +def test_read_special( + input_data, # pylint: disable=redefined-outer-name +): + """Test read_special function.""" + + result = devicemap.read_special(input_data) + assert result == {} # pylint: disable=C1803 + + +@pytest.mark.parametrize( + "content, result", + [ + # Test with a valid content string + ( + "Thu, 16 Nov 2023 07:17:45 +0100(219355 secs since boot)", + datetime(2023, 11, 16, 7, 17, 45, tzinfo=timezone(timedelta(hours=1))) + - timedelta(seconds=219355), + ), + # Test with an invalid content string (no seconds) + ("Thu, 16 Nov 2023 07:17:45 +0100(no secs since boot)", None), + # Test with an invalid content string (bad format) + ("bad format", None), + # Test with a content string that has an invalid date + ("Not a date (219355 secs since boot)", None), + # Test with a content string that has an invalid number of seconds + ("Thu, 16 Nov 2023 07:17:45 +0100(not a number secs since boot)", None), + ], +) +def test_read_uptime_string(content, result): + """Test read_uptime_string function.""" + + assert devicemap.read_uptime_string(content) == result + + +@pytest.mark.parametrize( + "boottime_return, expected_flags", + [ + (("boottime", False), {}), + (("boottime", True), {"reboot": True}), + ], +) +@patch("asusrouter.modules.endpoint.devicemap.process_boottime") +@patch("asusrouter.modules.endpoint.devicemap.process_ovpn") +def test_process( + mock_process_ovpn, + mock_process_boottime, + boottime_return, + expected_flags, +): + """Test process function.""" + + # Prepare the mock functions + mock_process_boottime.return_value = boottime_return + mock_process_ovpn.return_value = "openvpn" + + # Prepare the test data + data = {"history": {AsusData.BOOTTIME: AsusDataState(data="prev_boottime")}} + + # Call the function with the test data + result = devicemap.process(data) + + # Check the result + assert result == { + AsusData.DEVICEMAP: data, + AsusData.BOOTTIME: boottime_return[0], + AsusData.OPENVPN: "openvpn", + AsusData.FLAGS: expected_flags, + } + + # Check that the mock functions were called with the correct arguments + mock_process_boottime.assert_called_once_with(data, "prev_boottime") + mock_process_ovpn.assert_called_once_with(data) + + +@pytest.mark.parametrize( + "prev_boottime_delta, expected_result", + [ + (timedelta(seconds=1), ({"datetime": ANY}, False)), + (timedelta(seconds=3), ({"datetime": ANY}, True)), + ], +) +@patch("asusrouter.modules.endpoint.devicemap.read_uptime_string") +def test_process_boottime( + mock_read_uptime_string, prev_boottime_delta, expected_result +): + """Test process_boottime function.""" + + # Prepare the mock function + mock_read_uptime_string.return_value = datetime.now() + + # Prepare the test data + devicemap_data = {"sys": {"uptimeStr": "uptime string"}} + prev_boottime = {"datetime": datetime.now() - prev_boottime_delta} + + # Call the function with the test data + result = devicemap.process_boottime(devicemap_data, prev_boottime) + + # Check the result + assert result == expected_result + + # Check that the mock function was called with the correct argument + mock_read_uptime_string.assert_called_once_with("uptime string") + + +@patch("asusrouter.modules.endpoint.devicemap.AsusOVPNClient") +@patch("asusrouter.modules.endpoint.devicemap.AsusOVPNServer") +@patch("asusrouter.modules.endpoint.devicemap.safe_int") +def test_process_ovpn(mock_safe_int, mock_asusovpnserver, mock_asusovnclient): + """Test process_ovpn function.""" + + # Prepare the mock functions + mock_asusovnclient.return_value = MagicMock() + mock_asusovpnserver.return_value = MagicMock() + mock_safe_int.return_value = 0 + + # Prepare the test data + devicemap_data = { + "vpn": { + "client1_state": "state", + "client1_errno": "errno", + "server1_state": "state", + } + } + + # Call the function with the test data + result = devicemap.process_ovpn(devicemap_data) + + # Check the result + expected_result = { + "client": { + 1: { + "state": mock_asusovnclient.return_value, + "errno": 0, + } + }, + "server": { + 1: { + "state": mock_asusovpnserver.return_value, + } + }, + } + assert result == expected_result + + # Check that the mock functions were called with the correct arguments + mock_asusovnclient.assert_called_once_with(0) + mock_asusovpnserver.assert_called_once_with(0) + mock_safe_int.assert_any_call("state", default=0) + mock_safe_int.assert_any_call("errno") diff --git a/tests/modules/endpoint/test_endpoint.py b/tests/modules/endpoint/test_endpoint.py new file mode 100644 index 0000000..91affef --- /dev/null +++ b/tests/modules/endpoint/test_endpoint.py @@ -0,0 +1,183 @@ +"""Test for the main endpoint module.""" + +from unittest.mock import AsyncMock, MagicMock, patch + +import pytest + +from asusrouter.error import AsusRouter404Error +from asusrouter.modules.endpoint import ( + Endpoint, + _get_module, + check_available, + data_get, + data_set, + process, + read, +) + + +def test_get_module(): + """Test _get_module method.""" + + # Test valid endpoint + with patch("importlib.import_module") as mock_import: + mock_import.return_value = "mocked_module" + result = _get_module(Endpoint.RGB) + assert result == "mocked_module" + mock_import.assert_called_once_with("asusrouter.modules.endpoint.rgb") + + # Test invalid endpoint + with patch("importlib.import_module") as mock_import: + mock_import.side_effect = ModuleNotFoundError + result = _get_module(Endpoint.FIRMWARE) + assert result is None + mock_import.assert_called_once_with("asusrouter.modules.endpoint.firmware") + + +def test_read(): + """Test read method.""" + + # Mock the module and its read method + mock_module = MagicMock() + mock_module.read.return_value = {"mocked": "data"} + + # Test valid endpoint + with patch( + "asusrouter.modules.endpoint._get_module", return_value=mock_module + ) as mock_get_module: + result = read(Endpoint.FIRMWARE, "content") + assert result == {"mocked": "data"} + mock_get_module.assert_called_once_with(Endpoint.FIRMWARE) + mock_module.read.assert_called_once_with("content") + + # Test invalid endpoint + with patch( + "asusrouter.modules.endpoint._get_module", return_value=None + ) as mock_get_module: + result = read(Endpoint.RGB, "content") + assert result == {} + mock_get_module.assert_called_once_with(Endpoint.RGB) + + +@pytest.mark.parametrize( + "require_history,require_firmware,require_wlan,call_count", + [ + (True, False, False, 1), + (False, True, False, 1), + (False, False, True, 1), + (False, False, False, 0), + (True, True, True, 3), + ], +) +def test_process(require_history, require_firmware, require_wlan, call_count): + """Test process method.""" + + # Mock the module and its process method + mock_module = MagicMock() + mock_module.process.return_value = {"mocked": "data"} + + # Mock the data_set function + mock_data_set = MagicMock() + + # Define a side effect function for getattr + def getattr_side_effect(_, attr, default=None): + if attr == "REQUIRE_HISTORY": + return require_history + if attr == "REQUIRE_FIRMWARE": + return require_firmware + if attr == "REQUIRE_WLAN": + return require_wlan + return default + + # Test valid endpoint + with patch( + "asusrouter.modules.endpoint._get_module", return_value=mock_module + ), patch("asusrouter.modules.endpoint.data_set", mock_data_set), patch( + "asusrouter.modules.endpoint.getattr", side_effect=getattr_side_effect + ): + result = process(Endpoint.DEVICEMAP, {"key": "value"}) + assert result == {"mocked": "data"} + mock_module.process.assert_called_once_with({"key": "value"}) + assert mock_data_set.call_count == call_count + + +def test_process_no_module(): + """Test process method when no module is found.""" + + # Mock the _get_module function to return None + with patch( + "asusrouter.modules.endpoint._get_module", return_value=None + ) as mock_get_module: + result = process(Endpoint.RGB, {"key": "value"}) + assert result == {} + mock_get_module.assert_called_once_with(Endpoint.RGB) + + +def test_data_set(): + """Test data_set function.""" + + # Test data + data = {"key1": "value1"} + kwargs = {"key2": "value2", "key3": "value3"} + + # Call the function + result = data_set(data, **kwargs) + + # Check the result + assert result == {"key1": "value1", "key2": "value2", "key3": "value3"} + + +@pytest.mark.parametrize( + "data, key, expected, data_left", + [ + # Key exists + ({"key1": "value1", "key2": "value2"}, "key1", "value1", {"key2": "value2"}), + # Key does not exist + ( + {"key1": "value1", "key2": "value2"}, + "key3", + None, + {"key1": "value1", "key2": "value2"}, + ), + # Empty data + ({}, "key1", None, {}), + ], +) +def test_data_get(data, key, expected, data_left): + """Test data_get function.""" + + # Call the function + result = data_get(data, key) + + # Check the result + assert result == expected + assert data == data_left + + +@pytest.mark.asyncio +@pytest.mark.parametrize( + "api_query_return, expected_result", + [ + # Test case: status 200 + ((200, None, None), True), + # Test case: status not 200 + ((403, None, None), False), + # Test case: AsusRouter404Error is raised + (AsusRouter404Error(), False), + ], +) +async def test_check_available(api_query_return, expected_result): + """Test check_available function.""" + + # Mock the api_query function + api_query = AsyncMock() + if isinstance(api_query_return, Exception): + api_query.side_effect = api_query_return + else: + api_query.return_value = api_query_return + + # Call the function + result = await check_available(Endpoint.DEVICEMAP, api_query) + + # Check the result + assert result == expected_result diff --git a/tests/test_data/__init__.py b/tests/test_data/__init__.py new file mode 100644 index 0000000..86668bf --- /dev/null +++ b/tests/test_data/__init__.py @@ -0,0 +1 @@ +"""Test data for AsusRouter.""" diff --git a/tests/test_data/rt_ax88u_merlin_388/__init__.py b/tests/test_data/rt_ax88u_merlin_388/__init__.py new file mode 100644 index 0000000..d8ef8cc --- /dev/null +++ b/tests/test_data/rt_ax88u_merlin_388/__init__.py @@ -0,0 +1 @@ +"""Test data for RT-AX88U / Merlin 388.""" diff --git a/tests/test_data/rt_ax88u_merlin_388/devicemap_001.content b/tests/test_data/rt_ax88u_merlin_388/devicemap_001.content new file mode 100644 index 0000000..edabc3d --- /dev/null +++ b/tests/test_data/rt_ax88u_merlin_388/devicemap_001.content @@ -0,0 +1,86 @@ + + 2 +0 +0 + +monoClient= +wlc_state=0 +wlc_sbstate=0 +psta:wlc_state=0;wlc_state_auth=0; +wifi_hw_switch=1 +ddnsRet= +ddnsUpdate= +wan_line_state= +wlan0_radio_flag=1 +wlan1_radio_flag=1 +wlan2_radio_flag= +data_rate_info_2g="72 Mbps" +data_rate_info_5g="1921.5 Mbps" +data_rate_info_5g_2="0 Mbps" +wan_diag_state= +active_wan_unit=0 +wan0_enable=1 +wan1_enable=1 +wan0_realip_state=2 +wan1_realip_state=0 +wan0_ipaddr=111.222.111.222 +wan1_ipaddr=0.0.0.0 +wan0_realip_ip=111.222.111.222 +wan1_realip_ip= +vpnc_proto=disable +vpnc_state_t=0 +vpnc_sbstate_t=0 +vpn_client1_state=0 +vpn_client2_state= +vpn_client3_state=0 +vpn_client4_state=-1 +vpn_client5_state= +vpnd_state=0 +vpn_client1_errno=0 +vpn_client2_errno= +vpn_client3_errno=0 +vpn_client4_errno=8 +vpn_client5_errno= +vpn_server1_state=2 +vpn_server2_state= +uptimeStr=Sun, 19 Nov 2023 15:51:06 +0100(509356 secs since boot) +qtn_state= +'[]' +modem_enable=1 +2 +0 +0 +0 +0 +0 + +sim_state= +sim_signal= +sim_operation= +sim_isp= +roaming=0 +roaming_imsi= +sim_imsi= +g3err_pin= +pin_remaining_count= +modem_act_provider= +rx_bytes= +tx_bytes= +modem_sim_order= +dnsqmode= +wlc0_state= +wlc1_state= +rssi_2g= +rssi_5g= +rssi_5g_2= +link_internet=2 +diag_dblog_enable=0 +diag_dblog_remaining=0 +wlc2_state= +le_restart_httpd= +wan_bonding_speed="0" +wan_bonding_p1_status="0" +wan_bonding_p2_status="2" +lacp_wan=0 + + diff --git a/tests/test_data/rt_ax88u_merlin_388/devicemap_001.py b/tests/test_data/rt_ax88u_merlin_388/devicemap_001.py new file mode 100644 index 0000000..623aa36 --- /dev/null +++ b/tests/test_data/rt_ax88u_merlin_388/devicemap_001.py @@ -0,0 +1,114 @@ +"""Result of processing devicemap_001.content.""" + +from datetime import datetime + +from dateutil.tz import tzoffset + +from asusrouter import AsusData +from asusrouter.modules.openvpn import AsusOVPNClient, AsusOVPNServer + +expected_result = { + AsusData.DEVICEMAP: { + "wan": { + "status": "2", + "sbstatus": "0", + "auxstatus": "0", + "monoClient": None, + "wlc_state": "0", + "wlc_sbstate": "0", + "wifi_hw_switch": "1", + "ddnsRet": None, + "ddnsUpdate": None, + "line_state": None, + "wlan0_radio_flag": "1", + "wlan1_radio_flag": "1", + "wlan2_radio_flag": None, + "data_rate_info_2g": "72 Mbps", + "data_rate_info_5g": "1921.5 Mbps", + "data_rate_info_5g_2": "0 Mbps", + "diag_state": None, + "active_wan_unit": "0", + "wlc0_state": None, + "wlc1_state": None, + "rssi_2g": None, + "rssi_5g": None, + "rssi_5g_2": None, + "link_internet": "2", + "wlc2_state": None, + "le_restart_httpd": None, + }, + "wan0": { + "status": "2", + "sbstatus": "0", + "auxstatus": "0", + "enable": "1", + "realip_state": "2", + "ipaddr": "111.222.111.222", + "realip_ip": "111.222.111.222", + }, + "wan1": { + "status": "0", + "sbstatus": "0", + "auxstatus": "0", + "enable": "1", + "realip_state": "0", + "ipaddr": "0.0.0.0", + "realip_ip": None, + }, + "usb": {"status": "'[]'", "modem_enable": "1"}, + "vpn": { + "vpnc_proto": "disable", + "vpnc_state_t": "0", + "vpnc_sbstate_t": "0", + "client1_state": "0", + "client2_state": None, + "client3_state": "0", + "client4_state": "-1", + "client5_state": None, + "vpnd_state": "0", + "client1_errno": "0", + "client2_errno": None, + "client3_errno": "0", + "client4_errno": "8", + "client5_errno": None, + "server1_state": "2", + "server2_state": None, + }, + "sys": {"uptimeStr": "Sun, 19 Nov 2023 15:51:06 +0100(509356 secs since boot)"}, + "qtn": {"state": None}, + "sim": { + "state": None, + "signal": None, + "operation": None, + "isp": None, + "roaming": "0", + "roaming_imsi": None, + "imsi": None, + "g3err_pin": None, + "pin_remaining_count": None, + "modem_act_provider": None, + "rx_bytes": None, + "tx_bytes": None, + "modem_sim_order": None, + }, + "dhcp": {"dnsqmode": None}, + "diag": {"dblog_enable": "0", "dblog_remaining": "0"}, + }, + AsusData.BOOTTIME: { + "datetime": datetime(2023, 11, 13, 18, 21, 50, tzinfo=tzoffset(None, 3600)) + }, + AsusData.OPENVPN: { + "client": { + 1: {"state": AsusOVPNClient.DISCONNECTED, "errno": 0}, + 2: {"state": AsusOVPNClient.DISCONNECTED, "errno": None}, + 3: {"state": AsusOVPNClient.DISCONNECTED, "errno": 0}, + 4: {"state": AsusOVPNClient.ERROR, "errno": 8}, + 5: {"state": AsusOVPNClient.DISCONNECTED, "errno": None}, + }, + "server": { + 1: {"state": AsusOVPNServer.CONNECTED}, + 2: {"state": AsusOVPNServer.DISCONNECTED}, + }, + }, + AsusData.FLAGS: {}, +} diff --git a/tests/test_data/rt_ax88u_merlin_388/ethernet_ports_001.content b/tests/test_data/rt_ax88u_merlin_388/ethernet_ports_001.content new file mode 100644 index 0000000..d0f8f3d --- /dev/null +++ b/tests/test_data/rt_ax88u_merlin_388/ethernet_ports_001.content @@ -0,0 +1,2 @@ +get_wan_lan_status = { "portSpeed": { "WAN 0": "G", "LAN 1": "G", "LAN 2": "G", "LAN 3": "G", "LAN 4": "X", "LAN 5": "X", "LAN 6": "X", "LAN 7": "X", "LAN 8": "G" }, "portCount": { "wanCount": 1, "lanCount": 8 } }; + diff --git a/tests/test_data/rt_ax88u_merlin_388/ethernet_ports_001.py b/tests/test_data/rt_ax88u_merlin_388/ethernet_ports_001.py new file mode 100644 index 0000000..4054387 --- /dev/null +++ b/tests/test_data/rt_ax88u_merlin_388/ethernet_ports_001.py @@ -0,0 +1,49 @@ +"""Result of processing ethernet_ports_001.content.""" + +from asusrouter import AsusData +from asusrouter.modules.ports import PortSpeed, PortType + +expected_result = { + AsusData.PORTS: { + PortType.LAN: { + 1: { + "link_rate": PortSpeed.LINK_1000, + "state": True, + }, + 2: { + "link_rate": PortSpeed.LINK_1000, + "state": True, + }, + 3: { + "link_rate": PortSpeed.LINK_1000, + "state": True, + }, + 4: { + "link_rate": PortSpeed.LINK_DOWN, + "state": False, + }, + 5: { + "link_rate": PortSpeed.LINK_DOWN, + "state": False, + }, + 6: { + "link_rate": PortSpeed.LINK_DOWN, + "state": False, + }, + 7: { + "link_rate": PortSpeed.LINK_DOWN, + "state": False, + }, + 8: { + "link_rate": PortSpeed.LINK_1000, + "state": True, + }, + }, + PortType.WAN: { + 0: { + "link_rate": PortSpeed.LINK_1000, + "state": True, + }, + }, + } +} diff --git a/tests/test_data/rt_ax88u_merlin_388/firmware_001.content b/tests/test_data/rt_ax88u_merlin_388/firmware_001.content new file mode 100644 index 0000000..ab89116 --- /dev/null +++ b/tests/test_data/rt_ax88u_merlin_388/firmware_001.content @@ -0,0 +1,25 @@ +webs_state_update = '1'; +webs_state_error = '0'; +webs_state_info = '3004_388_4_0'; +webs_state_info_beta = ''; +webs_state_REQinfo = ''; +webs_state_flag = '0'; +webs_state_upgrade = ''; +webs_state_level = '0'; +sig_state_flag = '1'; +sig_state_update = '0'; +sig_state_upgrade = '1'; +sig_state_error = '0'; +sig_ver = '2.380'; +if(cfg_sync_support){ +cfg_check = ''; +cfg_upgrade = ''; +} +if(pipefw_support || urlfw_support){ +hndwr_status = '99'; +} +if(rbkfw_support){ +_rollback_info = JSON.parse(''); +rbk_count = Object.keys(_rollback_info).length; +} + diff --git a/tests/test_data/rt_ax88u_merlin_388/firmware_001.py b/tests/test_data/rt_ax88u_merlin_388/firmware_001.py new file mode 100644 index 0000000..0eed839 --- /dev/null +++ b/tests/test_data/rt_ax88u_merlin_388/firmware_001.py @@ -0,0 +1,26 @@ +"""Result of processing firmware_001.content.""" + +from asusrouter import AsusData + +expected_result = { + AsusData.FIRMWARE: { + "webs_state_error": "0", + "webs_state_info": "3004_388_4_0", + "webs_state_info_beta": "", + "webs_state_REQinfo": "", + "webs_state_flag": "0", + "webs_state_upgrade": "", + "webs_state_level": "0", + "sig_state_flag": "1", + "sig_state_update": "0", + "sig_state_upgrade": "1", + "sig_state_error": "0", + "sig_ver": "2.380", + "cfg_check": "", + "cfg_upgrade": "", + "hndwr_status": "99", + "state": True, + "current": "None", + "available": "3.0.0.4.388.4_0", + } +} diff --git a/tests/test_data/rt_ax88u_merlin_388/hook_001.content b/tests/test_data/rt_ax88u_merlin_388/hook_001.content new file mode 100644 index 0000000..a6baba1 --- /dev/null +++ b/tests/test_data/rt_ax88u_merlin_388/hook_001.content @@ -0,0 +1,5 @@ +{ +"cpu_usage":{"cpu1_total":"52557468","cpu1_usage":"4733197","cpu2_total":"52587131","cpu2_usage":"1731814","cpu3_total":"52621020","cpu3_usage":"1821047","cpu4_total":"52620879","cpu4_usage":"1720689"}, +"memory_usage":{"mem_total":"1048576","mem_free":"235464","mem_used":"813112"}, +"netdev":{ "BRIDGE_rx":"0x30a0b6bba","BRIDGE_tx":"0x134ce06712","INTERNET_rx":"0xe5d193f41","INTERNET_tx":"0x2000941dd","INTERNET_rx":"0xeb9a134fe","INTERNET_tx":"0x227d6b665","WIRED_rx":"0x39ddba16c","WIRED_tx":"0x80c855eba","WIRELESS0_rx":"0x1d09911e","WIRELESS0_tx":"0x610f01c4","WIRELESS1_rx":"0x2f570951","WIRELESS1_tx":"0x32ed23b8c","LACP1_rx":"0x0","LACP1_tx":"0x5e9c7a8c","LACP2_rx":"0xf5b08c18","LACP2_tx":"0x4408840"} +} diff --git a/tests/test_data/rt_ax88u_merlin_388/hook_001.py b/tests/test_data/rt_ax88u_merlin_388/hook_001.py new file mode 100644 index 0000000..508a7ba --- /dev/null +++ b/tests/test_data/rt_ax88u_merlin_388/hook_001.py @@ -0,0 +1,28 @@ +"""Result of processing hook_001.content.""" + +from asusrouter import AsusData + +expected_result = { + AsusData.CPU: { + "total": {"total": 210386498.0, "used": 10006747.0}, + 1: {"total": 52557468, "used": 4733197}, + 2: {"total": 52587131, "used": 1731814}, + 3: {"total": 52621020, "used": 1821047}, + 4: {"total": 52620879, "used": 1720689}, + }, + AsusData.NETWORK: { + "wan": {"rx": 63243891966, "tx": 9258317413}, + "wired": {"rx": 15533318508, "tx": 34569805498}, + "bridge": {"rx": 13053422522, "tx": 82894153490}, + "2ghz": {"rx": 487166238, "tx": 1628373444}, + "5ghz": {"rx": 794233169, "tx": 13670431628}, + "lacp1": {"rx": 0, "tx": 1587313292}, + "lacp2": {"rx": 4121988120, "tx": 71338048}, + }, + AsusData.RAM: { + "free": 235464, + "total": 1048576, + "used": 813112, + "usage": 77.54, + }, +} diff --git a/tests/test_data/rt_ax88u_merlin_388/hook_002.content b/tests/test_data/rt_ax88u_merlin_388/hook_002.content new file mode 100644 index 0000000..b7a82ed --- /dev/null +++ b/tests/test_data/rt_ax88u_merlin_388/hook_002.content @@ -0,0 +1,3 @@ +{ +"led_val":"0" +} diff --git a/tests/test_data/rt_ax88u_merlin_388/hook_002.py b/tests/test_data/rt_ax88u_merlin_388/hook_002.py new file mode 100644 index 0000000..fa17d6a --- /dev/null +++ b/tests/test_data/rt_ax88u_merlin_388/hook_002.py @@ -0,0 +1,6 @@ +"""Result of processing hook_002.content.""" + +from asusrouter import AsusData +from asusrouter.modules.led import AsusLED + +expected_result = {AsusData.LED: {"state": AsusLED.OFF}} diff --git a/tests/test_data/rt_ax88u_merlin_388/hook_003.content b/tests/test_data/rt_ax88u_merlin_388/hook_003.content new file mode 100644 index 0000000..8a56de4 --- /dev/null +++ b/tests/test_data/rt_ax88u_merlin_388/hook_003.content @@ -0,0 +1,7 @@ +{ +"MULTIFILTER_MAC":"00:00:00:00:00:01ᠸ:00:00:00:00:02ᠸ:00:00:00:00:03ᠸ:00:00:00:00:04ᠸ:00:00:00:00:05ᠸ:00:00:00:00:06ᠸ:00:00:00:00:07ᠸ:00:00:00:00:08", +"MULTIFILTER_DEVICENAME":"FakeName001>FakeName002>FakeName003>FakeName004>FakeName005>FakeName006>FakeName007>FakeName008", +"MULTIFILTER_ALL":"1", +"MULTIFILTER_MACFILTER_DAYTIME_V2":"W03E21000700<W04122000800>W03E21000700<W04122000800>W03E21000700<W04122000800>W03E21000700<W04122000800>W03E21000700<W04122000800>W03E21000700<W04122000800>W03E21000700<W04122000800>W03E21000700<W04122000800", +"MULTIFILTER_ENABLE":"2ɮɮɮɮɮɬɬ" +} diff --git a/tests/test_data/rt_ax88u_merlin_388/hook_003.py b/tests/test_data/rt_ax88u_merlin_388/hook_003.py new file mode 100644 index 0000000..75105e4 --- /dev/null +++ b/tests/test_data/rt_ax88u_merlin_388/hook_003.py @@ -0,0 +1,60 @@ +"""Result of processing hook_003.content.""" + +from asusrouter import AsusData +from asusrouter.modules.parental_control import AsusParentalControl, ParentalControlRule + +expected_result = { + AsusData.PARENTAL_CONTROL: { + "state": AsusParentalControl.ON, + "rules": { + "00:00:00:00:00:01": ParentalControlRule( + mac="00:00:00:00:00:01", + name="FakeName001", + type="block", + timemap="W03E21000700<W04122000800", + ), + "00:00:00:00:00:02": ParentalControlRule( + mac="00:00:00:00:00:02", + name="FakeName002", + type="block", + timemap="W03E21000700<W04122000800", + ), + "00:00:00:00:00:03": ParentalControlRule( + mac="00:00:00:00:00:03", + name="FakeName003", + type="block", + timemap="W03E21000700<W04122000800", + ), + "00:00:00:00:00:04": ParentalControlRule( + mac="00:00:00:00:00:04", + name="FakeName004", + type="block", + timemap="W03E21000700<W04122000800", + ), + "00:00:00:00:00:05": ParentalControlRule( + mac="00:00:00:00:00:05", + name="FakeName005", + type="block", + timemap="W03E21000700<W04122000800", + ), + "00:00:00:00:00:06": ParentalControlRule( + mac="00:00:00:00:00:06", + name="FakeName006", + type="block", + timemap="W03E21000700<W04122000800", + ), + "00:00:00:00:00:07": ParentalControlRule( + mac="00:00:00:00:00:07", + name="FakeName007", + type="disable", + timemap="W03E21000700<W04122000800", + ), + "00:00:00:00:00:08": ParentalControlRule( + mac="00:00:00:00:00:08", + name="FakeName008", + type="disable", + timemap="W03E21000700<W04122000800", + ), + }, + } +} diff --git a/tests/test_data/rt_ax88u_merlin_388/hook_004.content b/tests/test_data/rt_ax88u_merlin_388/hook_004.content new file mode 100644 index 0000000..c4c41d6 --- /dev/null +++ b/tests/test_data/rt_ax88u_merlin_388/hook_004.content @@ -0,0 +1,4 @@ +{ +"vts_rulelist":"", +"vts_enable_x":"0" +} diff --git a/tests/test_data/rt_ax88u_merlin_388/hook_004.py b/tests/test_data/rt_ax88u_merlin_388/hook_004.py new file mode 100644 index 0000000..e5451a9 --- /dev/null +++ b/tests/test_data/rt_ax88u_merlin_388/hook_004.py @@ -0,0 +1,6 @@ +"""Result of processing hook_004.content.""" + +from asusrouter import AsusData +from asusrouter.modules.port_forwarding import AsusPortForwarding + +expected_result = {AsusData.PORT_FORWARDING: {"state": AsusPortForwarding.OFF}} diff --git a/tests/test_data/rt_ax88u_merlin_388/hook_005.content b/tests/test_data/rt_ax88u_merlin_388/hook_005.content new file mode 100644 index 0000000..40badc6 --- /dev/null +++ b/tests/test_data/rt_ax88u_merlin_388/hook_005.content @@ -0,0 +1,63 @@ +{ +"vpnc_clientlist":"", +"wgc1_enable":"", +"wgc1_nat":"", +"wgc1_priv":"", +"wgc1_addr":"", +"wgc1_dns":"", +"wgc1_mtu":"", +"wgc1_ppub":"", +"wgc1_psk":"", +"wgc1_aips":"", +"wgc1_ep_addr":"", +"wgc1_ep_port":"", +"wgc1_alive":"", +"wgc2_enable":"", +"wgc2_nat":"", +"wgc2_priv":"", +"wgc2_addr":"", +"wgc2_dns":"", +"wgc2_mtu":"", +"wgc2_ppub":"", +"wgc2_psk":"", +"wgc2_aips":"", +"wgc2_ep_addr":"", +"wgc2_ep_port":"", +"wgc2_alive":"", +"wgc3_enable":"", +"wgc3_nat":"", +"wgc3_priv":"", +"wgc3_addr":"", +"wgc3_dns":"", +"wgc3_mtu":"", +"wgc3_ppub":"", +"wgc3_psk":"", +"wgc3_aips":"", +"wgc3_ep_addr":"", +"wgc3_ep_port":"", +"wgc3_alive":"", +"wgc4_enable":"", +"wgc4_nat":"", +"wgc4_priv":"", +"wgc4_addr":"", +"wgc4_dns":"", +"wgc4_mtu":"", +"wgc4_ppub":"", +"wgc4_psk":"", +"wgc4_aips":"", +"wgc4_ep_addr":"", +"wgc4_ep_port":"", +"wgc4_alive":"", +"wgc5_enable":"1", +"wgc5_nat":"1", +"wgc5_priv":"PrIvAtE KeY", +"wgc5_addr":"10.1.1.11/32", +"wgc5_dns":"", +"wgc5_mtu":"", +"wgc5_ppub":"PuBlIk kEy", +"wgc5_psk":"PrE-ShArEd kEy", +"wgc5_aips":"10.1.1.0/24,192.168.1.0/24", +"wgc5_ep_addr":"111.222.111.222", +"wgc5_ep_port":"443", +"wgc5_alive":"25" +} diff --git a/tests/test_data/rt_ax88u_merlin_388/hook_005.py b/tests/test_data/rt_ax88u_merlin_388/hook_005.py new file mode 100644 index 0000000..98c2ec3 --- /dev/null +++ b/tests/test_data/rt_ax88u_merlin_388/hook_005.py @@ -0,0 +1,61 @@ +"""Result of processing hook_005.content.""" + +from asusrouter import AsusData +from asusrouter.modules.endpoint.error import AccessError +from asusrouter.modules.vpnc import AsusVPNC, AsusVPNType + +expected_result = { + AsusData.OPENVPN_CLIENT: { + 1: {"state": AsusVPNC.UNKNOWN, "error": AccessError.NO_ERROR}, + 2: {"state": AsusVPNC.UNKNOWN, "error": AccessError.NO_ERROR}, + 3: {"state": AsusVPNC.UNKNOWN, "error": AccessError.NO_ERROR}, + 4: {"state": AsusVPNC.UNKNOWN, "error": AccessError.NO_ERROR}, + 5: {"state": AsusVPNC.UNKNOWN, "error": AccessError.NO_ERROR}, + }, + AsusData.VPNC: { + AsusVPNType.OPENVPN: { + 1: {"state": AsusVPNC.UNKNOWN, "error": AccessError.NO_ERROR}, + 2: {"state": AsusVPNC.UNKNOWN, "error": AccessError.NO_ERROR}, + 3: {"state": AsusVPNC.UNKNOWN, "error": AccessError.NO_ERROR}, + 4: {"state": AsusVPNC.UNKNOWN, "error": AccessError.NO_ERROR}, + 5: {"state": AsusVPNC.UNKNOWN, "error": AccessError.NO_ERROR}, + }, + AsusVPNType.WIREGUARD: { + 5: { + "state": True, + "nat": True, + "private_key": "PrIvAtE KeY", + "address": "10.1.1.11/32", + "public_key": "PuBlIk kEy", + "psk": "PrE-ShArEd kEy", + "allowed_ips": ["10.1.1.0/24", "192.168.1.0/24"], + "endpoint_address": "111.222.111.222", + "endpoint_port": 443, + "keep_alive": 25, + }, + 1: {"state": AsusVPNC.UNKNOWN, "error": AccessError.NO_ERROR}, + 2: {"state": AsusVPNC.UNKNOWN, "error": AccessError.NO_ERROR}, + 3: {"state": AsusVPNC.UNKNOWN, "error": AccessError.NO_ERROR}, + 4: {"state": AsusVPNC.UNKNOWN, "error": AccessError.NO_ERROR}, + }, + }, + AsusData.VPNC_CLIENTLIST: "", + AsusData.WIREGUARD_CLIENT: { + 5: { + "state": True, + "nat": True, + "private_key": "PrIvAtE KeY", + "address": "10.1.1.11/32", + "public_key": "PuBlIk kEy", + "psk": "PrE-ShArEd kEy", + "allowed_ips": ["10.1.1.0/24", "192.168.1.0/24"], + "endpoint_address": "111.222.111.222", + "endpoint_port": 443, + "keep_alive": 25, + }, + 1: {"state": AsusVPNC.UNKNOWN, "error": AccessError.NO_ERROR}, + 2: {"state": AsusVPNC.UNKNOWN, "error": AccessError.NO_ERROR}, + 3: {"state": AsusVPNC.UNKNOWN, "error": AccessError.NO_ERROR}, + 4: {"state": AsusVPNC.UNKNOWN, "error": AccessError.NO_ERROR}, + }, +} diff --git a/tests/test_data/rt_ax88u_merlin_388/hook_006.content b/tests/test_data/rt_ax88u_merlin_388/hook_006.content new file mode 100644 index 0000000..52f1a77 --- /dev/null +++ b/tests/test_data/rt_ax88u_merlin_388/hook_006.content @@ -0,0 +1,48 @@ +{ +"get_wan_unit":0, +"link_internet":"2", +"link_wan":"1", +"link_wan1":"0", +"wans_mode":"fo", +"wans_dualwan":"wan none", +"bond_wan":"0", +"wanports_bond":"4 0", +"wan0_auxstate_t":"0", +"wan0_primary":"1", +"wan0_proto":"pppoe", +"wan0_realip_ip":"111.222.111.222", +"wan0_realip_state":"2", +"wan0_state_t":"2", +"wan0_sbstate_t":"0", +"wan1_auxstate_t":"0", +"wan1_primary":"0", +"wan1_proto":"dhcp", +"wan1_realip_ip":"", +"wan1_realip_state":"0", +"wan1_state_t":"0", +"wan1_sbstate_t":"0", +"wan0_dns":"222.111.222.110 222.111.222.111", +"wan0_expires":"", +"wan0_gateway":"111.222.111.1", +"wan0_ipaddr":"111.222.111.222", +"wan0_lease":"", +"wan0_netmask":"255.255.255.255", +"wan0_xdns":"192.168.1.254", +"wan0_xexpires":"604829", +"wan0_xgateway":"192.168.1.254", +"wan0_xipaddr":"192.168.1.2", +"wan0_xlease":"86400", +"wan0_xnetmask":"255.255.255.0", +"wan1_dns":"", +"wan1_expires":"", +"wan1_gateway":"0.0.0.0", +"wan1_ipaddr":"0.0.0.0", +"wan1_lease":"", +"wan1_netmask":"0.0.0.0", +"wan1_xdns":"", +"wan1_xexpires":"", +"wan1_xgateway":"0.0.0.0", +"wan1_xipaddr":"0.0.0.0", +"wan1_xlease":"", +"wan1_xnetmask":"0.0.0.0" +} diff --git a/tests/test_data/rt_ax88u_merlin_388/hook_006.py b/tests/test_data/rt_ax88u_merlin_388/hook_006.py new file mode 100644 index 0000000..003073d --- /dev/null +++ b/tests/test_data/rt_ax88u_merlin_388/hook_006.py @@ -0,0 +1,74 @@ +"""Result of processing hook_006.content.""" + +from asusrouter import AsusData +from asusrouter.modules.connection import ConnectionState, ConnectionStatus +from asusrouter.modules.endpoint.wan import AsusDualWAN +from asusrouter.modules.ip_address import IPAddressType + +expected_result = { + AsusData.WAN: { + "internet": { + "unit": 0, + "link": ConnectionStatus.CONNECTED, + "ip_address": "111.222.111.222", + }, + 0: { + "auxstate": ConnectionStatus.DISCONNECTED, + "primary": True, + "protocol": IPAddressType.PPPOE, + "real_ip": "111.222.111.222", + "real_ip_state": None, + "state": ConnectionStatus.CONNECTED, + "bstate": ConnectionStatus.DISCONNECTED, + "main": { + "dns": ["222.111.222.110", "222.111.222.111"], + "expires": None, + "gateway": "111.222.111.1", + "ip_address": "111.222.111.222", + "lease": None, + "mask": "255.255.255.255", + }, + "extra": { + "dns": ["192.168.1.254"], + "expires": 604829, + "gateway": "192.168.1.254", + "ip_address": "192.168.1.2", + "lease": 86400, + "mask": "255.255.255.0", + }, + "link": ConnectionState.CONNECTED, + }, + 1: { + "auxstate": ConnectionStatus.DISCONNECTED, + "primary": False, + "protocol": IPAddressType.DHCP, + "real_ip": "", + "real_ip_state": False, + "state": ConnectionStatus.DISCONNECTED, + "bstate": ConnectionStatus.DISCONNECTED, + "main": { + "dns": [], + "expires": None, + "gateway": "0.0.0.0", + "ip_address": "0.0.0.0", + "lease": None, + "mask": "0.0.0.0", + }, + "extra": { + "dns": [], + "expires": None, + "gateway": "0.0.0.0", + "ip_address": "0.0.0.0", + "lease": None, + "mask": "0.0.0.0", + }, + "link": ConnectionState.DISCONNECTED, + }, + "aggregation": {"state": False, "ports": ["4", "0"]}, + "dualwan": { + "mode": AsusDualWAN.FAILOVER, + "priority": ["wan", None], + "state": False, + }, + } +} diff --git a/tests/test_data/rt_ax88u_merlin_388/hook_007.content b/tests/test_data/rt_ax88u_merlin_388/hook_007.content new file mode 100644 index 0000000..25437dd --- /dev/null +++ b/tests/test_data/rt_ax88u_merlin_388/hook_007.content @@ -0,0 +1,64 @@ +{ +"get_wgsc_status":{ "client_status": [ { "pub": "PuB001", "index": 3, "status": 0 }, { "pub": "PuB002", "index": 2, "status": 0 }, { "pub": "PuB003", "index": 5, "status": 0 }, { "pub": "PuB004", "index": 6, "status": 0 }, { "pub": "PuB005", "index": 1, "status": 0 }, { "pub": "PuB006", "index": 4, "status": 0 } ] } +, +"wgs_enable":"1", +"wgs_lanaccess":"1", +"wgs_addr":"10.1.0.1/24", +"wgs_port":"443", +"wgs_dns":"1", +"wgs_nat6":"1", +"wgs_psk":"1", +"wgs_alive":"25", +"wgs_priv":"PrIvAtE KeY", +"wgs_pub":"PuBlIk kEy", +"wgs1_c1_enable":"1", +"wgs1_c1_name":"FakeName001", +"wgs1_c1_addr":"10.1.0.2/32", +"wgs1_c1_aips":"10.1.0.2/32", +"wgs1_c1_caips":"192.168.1.0/24,10.1.0.0/24", +"wgs1_c2_enable":"1", +"wgs1_c2_name":"FakeName002", +"wgs1_c2_addr":"10.1.0.3/32", +"wgs1_c2_aips":"10.1.0.3/32", +"wgs1_c2_caips":"192.168.1.0/24,10.1.0.0/24", +"wgs1_c3_enable":"1", +"wgs1_c3_name":"FakeName003", +"wgs1_c3_addr":"10.1.0.4/32", +"wgs1_c3_aips":"10.1.0.4/32", +"wgs1_c3_caips":"192.168.1.0/24,10.1.0.0/24", +"wgs1_c4_enable":"1", +"wgs1_c4_name":"FakeName004", +"wgs1_c4_addr":"10.1.0.5/32", +"wgs1_c4_aips":"10.1.0.5/32", +"wgs1_c4_caips":"192.168.1.0/24,10.1.0.0/24", +"wgs1_c5_enable":"1", +"wgs1_c5_name":"FakeName005", +"wgs1_c5_addr":"10.1.0.6/32", +"wgs1_c5_aips":"10.1.0.6/32", +"wgs1_c5_caips":"192.168.1.0/24,10.1.0.0/24", +"wgs1_c6_enable":"1", +"wgs1_c6_name":"FakeName006", +"wgs1_c6_addr":"10.1.0.7/32", +"wgs1_c6_aips":"10.1.0.7/32", +"wgs1_c6_caips":"192.168.1.0/24,10.1.0.0/24", +"wgs1_c7_enable":"", +"wgs1_c7_name":"", +"wgs1_c7_addr":"", +"wgs1_c7_aips":"", +"wgs1_c7_caips":"", +"wgs1_c8_enable":"", +"wgs1_c8_name":"", +"wgs1_c8_addr":"", +"wgs1_c8_aips":"", +"wgs1_c8_caips":"", +"wgs1_c9_enable":"", +"wgs1_c9_name":"", +"wgs1_c9_addr":"", +"wgs1_c9_aips":"", +"wgs1_c9_caips":"", +"wgs1_c10_enable":"", +"wgs1_c10_name":"", +"wgs1_c10_addr":"", +"wgs1_c10_aips":"", +"wgs1_c10_caips":"" +} diff --git a/tests/test_data/rt_ax88u_merlin_388/hook_007.py b/tests/test_data/rt_ax88u_merlin_388/hook_007.py new file mode 100644 index 0000000..b63df21 --- /dev/null +++ b/tests/test_data/rt_ax88u_merlin_388/hook_007.py @@ -0,0 +1,72 @@ +"""Result of processing hook_007.content.""" + +from asusrouter import AsusData +from asusrouter.modules.connection import ConnectionState +from asusrouter.modules.wireguard import AsusWireGuardServer + +expected_result = { + AsusData.WIREGUARD_SERVER: { + 1: { + "state": AsusWireGuardServer.ON, + "lan_access": True, + "address": "10.1.0.1/24", + "port": 443, + "dns": True, + "nat6": True, + "psk": True, + "keep_alive": 25, + "private_key": "PrIvAtE KeY", + "public_key": "PuBlIk kEy", + "clients": { + 1: { + "enabled": True, + "name": "FakeName001", + "address": "10.1.0.2/32", + "allowed_ips": ["10.1.0.2/32"], + "client_allowed_ips": ["192.168.1.0/24", "10.1.0.0/24"], + "state": ConnectionState.DISCONNECTED, + }, + 2: { + "enabled": True, + "name": "FakeName002", + "address": "10.1.0.3/32", + "allowed_ips": ["10.1.0.3/32"], + "client_allowed_ips": ["192.168.1.0/24", "10.1.0.0/24"], + "state": ConnectionState.DISCONNECTED, + }, + 3: { + "enabled": True, + "name": "FakeName003", + "address": "10.1.0.4/32", + "allowed_ips": ["10.1.0.4/32"], + "client_allowed_ips": ["192.168.1.0/24", "10.1.0.0/24"], + "state": ConnectionState.DISCONNECTED, + }, + 4: { + "enabled": True, + "name": "FakeName004", + "address": "10.1.0.5/32", + "allowed_ips": ["10.1.0.5/32"], + "client_allowed_ips": ["192.168.1.0/24", "10.1.0.0/24"], + "state": ConnectionState.DISCONNECTED, + }, + 5: { + "enabled": True, + "name": "FakeName005", + "address": "10.1.0.6/32", + "allowed_ips": ["10.1.0.6/32"], + "client_allowed_ips": ["192.168.1.0/24", "10.1.0.0/24"], + "state": ConnectionState.DISCONNECTED, + }, + 6: { + "enabled": True, + "name": "FakeName006", + "address": "10.1.0.7/32", + "allowed_ips": ["10.1.0.7/32"], + "client_allowed_ips": ["192.168.1.0/24", "10.1.0.0/24"], + "state": ConnectionState.DISCONNECTED, + }, + }, + } + } +} diff --git a/tests/test_data/rt_ax88u_merlin_388/onboarding_001.content b/tests/test_data/rt_ax88u_merlin_388/onboarding_001.content new file mode 100644 index 0000000..b7b0842 --- /dev/null +++ b/tests/test_data/rt_ax88u_merlin_388/onboarding_001.content @@ -0,0 +1,9 @@ +get_onboardinglist = [{}][0]; +get_cfg_clientlist = [[{"alias":"Living Room","model_name":"RT-AX88U","ui_model_name":"RT-AX88U","icon_model_name":"","product_id":"RT-AX88U","frs_model_name":"","fwver":"3.0.0.4.388.4_0","newfwver":"","ip":"192.168.1.1","mac":"00:00:00:00:00:00","online":"1","ap2g":"00:00:00:00:00:00","ap5g":"00:00:00:00:00:01","ap5g1":"","apdwb":"","ap6g":"","wired_mac":[],"pap2g":"","rssi2g":"","pap5g":"","rssi5g":"","pap6g":"","rssi6g":"","level":"0","re_path":"0","config":{},"sta2g":"","sta5g":"","sta6g":"","capability":{"3":3,"2":1,"16":1,"17":1,"19":1,"20":1,"1":31,"4":10111,"22":3,"23":1},"ap2g_ssid":"WiFi2","ap5g_ssid":"WiFi5","ap5g1_ssid":"","ap6g_ssid":"","pap2g_ssid":"","pap5g_ssid":"","pap6g_ssid":"","wired_port":{},"plc_status":{},"moca_status":{},"band_num":"2","tcode":"EU/01","misc_info":{"2":"1"},"ap2g_fh":"00:00:00:00:00:00","ap5g_fh":"00:00:00:00:00:01","ap5g1_fh":"","ap6g_fh":"","ap2g_ssid_fh":"WiFi2","ap5g_ssid_fh":"WiFi5","ap5g1_ssid_fh":"","ap6g_ssid_fh":"","band_info":{"0":{"unit":0},"1":{"unit":1}}}]][0]; +get_onboardingstatus = [{"cfg_obstatus":"1","cfg_obresult":"","cfg_newre":"","cfg_wifi_quality":"-65","cfg_obstart":"","cfg_obcurrent":"1700405465","cfg_obtimeout":"","cfg_obmodel":"","cfg_ui_obmodel":"","cfg_obrssi":"-23","cfg_obcount":"0","cfg_obreboottime":"","cfg_obstage":"0","cfg_obfailresult":"0","cfg_re_maxnum":"9","cfg_recount":"0","cfg_ready":"1"}][0]; +get_wclientlist = [{"00:00:00:00:00:00":{"2G":["00:00:00:00:00:02","00:00:00:00:00:03","00:00:00:00:00:04","00:00:00:00:00:05","00:00:00:00:00:06","00:00:00:00:00:07","00:00:00:00:00:08","00:00:00:00:00:09","00:00:00:00:00:10","00:00:00:00:00:11","00:00:00:00:00:12"],"5G":["00:00:00:00:00:13","00:00:00:00:00:14","00:00:00:00:00:15"]}}][0]; +get_wiredclientlist = [{ "00:00:00:00:00:00": [ "00:00:00:00:00:16", "00:00:00:00:00:17", "00:00:00:00:00:18", "00:00:00:00:00:19", "00:00:00:00:00:20" ] }][0]; +get_allclientlist = [{"00:00:00:00:00:00":{"2G":{"00:00:00:00:00:02":{"ip":"192.168.1.2","rssi":"-34"},"00:00:00:00:00:03":{"ip":"192.168.1.3","rssi":"-47"},"00:00:00:00:00:04":{"ip":"192.168.1.4","rssi":"-58"},"00:00:00:00:00:05":{"ip":"192.168.1.5","rssi":"-50"},"00:00:00:00:00:06":{"ip":"192.168.1.6","rssi":"-26"},"00:00:00:00:00:07":{"ip":"192.168.1.7","rssi":"-52"},"00:00:00:00:00:08":{"ip":"192.168.1.8","rssi":"-41"},"00:00:00:00:00:09":{"ip":"192.168.1.9","rssi":"-41"},"00:00:00:00:00:10":{"ip":"192.168.1.10","rssi":"-44"},"00:00:00:00:00:11":{"ip":"192.168.1.11","rssi":"-43"},"00:00:00:00:00:12":{"ip":"192.168.1.12","rssi":"-53"}},"5G":{"00:00:00:00:00:13":{"ip":"192.168.1.13","rssi":"-43"},"00:00:00:00:00:14":{"ip":"192.168.1.14","rssi":"-27"},"00:00:00:00:00:15":{"ip":"192.168.1.15","rssi":"-40"}},"wired_mac":{"00:00:00:00:00:16":{"ip":"192.168.1.16"},"00:00:00:00:00:17":{"ip":"192.168.1.17"},"00:00:00:00:00:18":{"ip":"192.168.1.18"},"00:00:00:00:00:19":{"ip":"192.168.1.19"},"00:00:00:00:00:20":{"ip":"192.168.1.20"}}}}][0]; +cfg_note = "1"; +cfg_obre = ""; + diff --git a/tests/test_data/rt_ax88u_merlin_388/onboarding_001.py b/tests/test_data/rt_ax88u_merlin_388/onboarding_001.py new file mode 100644 index 0000000..67b1832 --- /dev/null +++ b/tests/test_data/rt_ax88u_merlin_388/onboarding_001.py @@ -0,0 +1,197 @@ +"""Result of processing onboarding_001.content.""" + +from asusrouter import AsusData +from asusrouter.modules.aimesh import AiMeshDevice + +expected_result = { + AsusData.AIMESH: { + "00:00:00:00:00:00": AiMeshDevice( + status=True, + alias="Living Room", + model="RT-AX88U", + product_id="RT-AX88U", + ip="192.168.1.1", + fw="3.0.0.4.388.4_0", + fw_new=None, + mac="00:00:00:00:00:00", + ap={"2ghz": "00:00:00:00:00:00", "5ghz": "00:00:00:00:00:01"}, + parent={}, + type="router", + level=0, + config={}, + ) + }, + AsusData.CLIENTS: { + "00:00:00:00:00:02": { + "connection_type": 1, + "guest": 0, + "ip": "192.168.1.2", + "mac": "00:00:00:00:00:02", + "node": "00:00:00:00:00:00", + "online": True, + "rssi": "-34", + }, + "00:00:00:00:00:03": { + "connection_type": 1, + "guest": 0, + "ip": "192.168.1.3", + "mac": "00:00:00:00:00:03", + "node": "00:00:00:00:00:00", + "online": True, + "rssi": "-47", + }, + "00:00:00:00:00:04": { + "connection_type": 1, + "guest": 0, + "ip": "192.168.1.4", + "mac": "00:00:00:00:00:04", + "node": "00:00:00:00:00:00", + "online": True, + "rssi": "-58", + }, + "00:00:00:00:00:05": { + "connection_type": 1, + "guest": 0, + "ip": "192.168.1.5", + "mac": "00:00:00:00:00:05", + "node": "00:00:00:00:00:00", + "online": True, + "rssi": "-50", + }, + "00:00:00:00:00:06": { + "connection_type": 1, + "guest": 0, + "ip": "192.168.1.6", + "mac": "00:00:00:00:00:06", + "node": "00:00:00:00:00:00", + "online": True, + "rssi": "-26", + }, + "00:00:00:00:00:07": { + "connection_type": 1, + "guest": 0, + "ip": "192.168.1.7", + "mac": "00:00:00:00:00:07", + "node": "00:00:00:00:00:00", + "online": True, + "rssi": "-52", + }, + "00:00:00:00:00:08": { + "connection_type": 1, + "guest": 0, + "ip": "192.168.1.8", + "mac": "00:00:00:00:00:08", + "node": "00:00:00:00:00:00", + "online": True, + "rssi": "-41", + }, + "00:00:00:00:00:09": { + "connection_type": 1, + "guest": 0, + "ip": "192.168.1.9", + "mac": "00:00:00:00:00:09", + "node": "00:00:00:00:00:00", + "online": True, + "rssi": "-41", + }, + "00:00:00:00:00:10": { + "connection_type": 1, + "guest": 0, + "ip": "192.168.1.10", + "mac": "00:00:00:00:00:10", + "node": "00:00:00:00:00:00", + "online": True, + "rssi": "-44", + }, + "00:00:00:00:00:11": { + "connection_type": 1, + "guest": 0, + "ip": "192.168.1.11", + "mac": "00:00:00:00:00:11", + "node": "00:00:00:00:00:00", + "online": True, + "rssi": "-43", + }, + "00:00:00:00:00:12": { + "connection_type": 1, + "guest": 0, + "ip": "192.168.1.12", + "mac": "00:00:00:00:00:12", + "node": "00:00:00:00:00:00", + "online": True, + "rssi": "-53", + }, + "00:00:00:00:00:13": { + "connection_type": 2, + "guest": 0, + "ip": "192.168.1.13", + "mac": "00:00:00:00:00:13", + "node": "00:00:00:00:00:00", + "online": True, + "rssi": "-43", + }, + "00:00:00:00:00:14": { + "connection_type": 2, + "guest": 0, + "ip": "192.168.1.14", + "mac": "00:00:00:00:00:14", + "node": "00:00:00:00:00:00", + "online": True, + "rssi": "-27", + }, + "00:00:00:00:00:15": { + "connection_type": 2, + "guest": 0, + "ip": "192.168.1.15", + "mac": "00:00:00:00:00:15", + "node": "00:00:00:00:00:00", + "online": True, + "rssi": "-40", + }, + "00:00:00:00:00:16": { + "connection_type": 0, + "guest": 0, + "ip": "192.168.1.16", + "mac": "00:00:00:00:00:16", + "node": "00:00:00:00:00:00", + "online": True, + "rssi": None, + }, + "00:00:00:00:00:17": { + "connection_type": 0, + "guest": 0, + "ip": "192.168.1.17", + "mac": "00:00:00:00:00:17", + "node": "00:00:00:00:00:00", + "online": True, + "rssi": None, + }, + "00:00:00:00:00:18": { + "connection_type": 0, + "guest": 0, + "ip": "192.168.1.18", + "mac": "00:00:00:00:00:18", + "node": "00:00:00:00:00:00", + "online": True, + "rssi": None, + }, + "00:00:00:00:00:19": { + "connection_type": 0, + "guest": 0, + "ip": "192.168.1.19", + "mac": "00:00:00:00:00:19", + "node": "00:00:00:00:00:00", + "online": True, + "rssi": None, + }, + "00:00:00:00:00:20": { + "connection_type": 0, + "guest": 0, + "ip": "192.168.1.20", + "mac": "00:00:00:00:00:20", + "node": "00:00:00:00:00:00", + "online": True, + "rssi": None, + }, + }, +} diff --git a/tests/test_data/rt_ax88u_merlin_388/port_status_001.content b/tests/test_data/rt_ax88u_merlin_388/port_status_001.content new file mode 100644 index 0000000..88d7bf3 --- /dev/null +++ b/tests/test_data/rt_ax88u_merlin_388/port_status_001.content @@ -0,0 +1 @@ +{ "node_info": { "7C:10:C9:03:6D:90": { "cd_good_to_go": "1" } }, "port_info": { "7C:10:C9:03:6D:90": { "W0": { "is_on": "1", "cap": "1073741825", "max_rate": "1000", "link_rate": "1000", "brown": "0", "brown_len": "0", "blue": "0", "blue_len": "0", "green": "0", "green_len": "0", "orange": "0", "orange_len": "0", "linkrecover": "1", "timeout": "1" }, "L1": { "is_on": "1", "cap": "2", "max_rate": "1000", "link_rate": "1000", "brown": "0", "brown_len": "0", "blue": "0", "blue_len": "0", "green": "0", "green_len": "0", "orange": "0", "orange_len": "0", "linkrecover": "1", "timeout": "1" }, "L2": { "is_on": "1", "cap": "2", "max_rate": "1000", "link_rate": "1000", "brown": "0", "brown_len": "0", "blue": "0", "blue_len": "0", "green": "0", "green_len": "0", "orange": "0", "orange_len": "0", "linkrecover": "1", "timeout": "1" }, "L3": { "is_on": "1", "cap": "2", "max_rate": "1000", "link_rate": "1000", "brown": "0", "brown_len": "0", "blue": "0", "blue_len": "0", "green": "0", "green_len": "0", "orange": "0", "orange_len": "0", "linkrecover": "1", "timeout": "1" }, "L4": { "is_on": "0", "cap": "2", "max_rate": "1000", "link_rate": "0", "brown": "0", "brown_len": "0", "blue": "0", "blue_len": "0", "green": "0", "green_len": "0", "orange": "0", "orange_len": "0", "linkrecover": "1", "timeout": "1" }, "L5": { "is_on": "0", "cap": "2", "max_rate": "1000", "link_rate": "0", "brown": "0", "brown_len": "0", "blue": "0", "blue_len": "0", "green": "0", "green_len": "0", "orange": "0", "orange_len": "0", "linkrecover": "1", "timeout": "1" }, "L6": { "is_on": "0", "cap": "2", "max_rate": "1000", "link_rate": "0", "brown": "0", "brown_len": "0", "blue": "0", "blue_len": "0", "green": "0", "green_len": "0", "orange": "0", "orange_len": "0", "linkrecover": "1", "timeout": "1" }, "L7": { "is_on": "0", "cap": "2", "max_rate": "1000", "link_rate": "0", "brown": "0", "brown_len": "0", "blue": "0", "blue_len": "0", "green": "0", "green_len": "0", "orange": "0", "orange_len": "0", "linkrecover": "1", "timeout": "1" }, "L8": { "is_on": "1", "cap": "2", "max_rate": "1000", "link_rate": "1000", "brown": "0", "brown_len": "0", "blue": "0", "blue_len": "0", "green": "0", "green_len": "0", "orange": "0", "orange_len": "0", "linkrecover": "1", "timeout": "1" }, "U1": { "is_on": "0", "cap": "128", "max_rate": "5000", "link_rate": "0" }, "U2": { "is_on": "0", "cap": "128", "max_rate": "5000", "link_rate": "0" } } } } \ No newline at end of file diff --git a/tests/test_data/rt_ax88u_merlin_388/port_status_001.py b/tests/test_data/rt_ax88u_merlin_388/port_status_001.py new file mode 100644 index 0000000..d585cb6 --- /dev/null +++ b/tests/test_data/rt_ax88u_merlin_388/port_status_001.py @@ -0,0 +1,162 @@ +"""Result of processing port_status_001.content.""" + +from asusrouter import AsusData +from asusrouter.modules.ports import PortType + +expected_result = { + AsusData.NODE_INFO: {"cd_good_to_go": "1"}, + AsusData.PORTS: { + PortType.LAN: { + 1: { + "cap": "2", + "max_rate": 1000, + "link_rate": 1000, + "brown": "0", + "brown_len": "0", + "blue": "0", + "blue_len": "0", + "green": "0", + "green_len": "0", + "orange": "0", + "orange_len": "0", + "linkrecover": "1", + "timeout": "1", + "state": True, + }, + 2: { + "cap": "2", + "max_rate": 1000, + "link_rate": 1000, + "brown": "0", + "brown_len": "0", + "blue": "0", + "blue_len": "0", + "green": "0", + "green_len": "0", + "orange": "0", + "orange_len": "0", + "linkrecover": "1", + "timeout": "1", + "state": True, + }, + 3: { + "cap": "2", + "max_rate": 1000, + "link_rate": 1000, + "brown": "0", + "brown_len": "0", + "blue": "0", + "blue_len": "0", + "green": "0", + "green_len": "0", + "orange": "0", + "orange_len": "0", + "linkrecover": "1", + "timeout": "1", + "state": True, + }, + 4: { + "cap": "2", + "max_rate": 1000, + "link_rate": 0, + "brown": "0", + "brown_len": "0", + "blue": "0", + "blue_len": "0", + "green": "0", + "green_len": "0", + "orange": "0", + "orange_len": "0", + "linkrecover": "1", + "timeout": "1", + "state": False, + }, + 5: { + "cap": "2", + "max_rate": 1000, + "link_rate": 0, + "brown": "0", + "brown_len": "0", + "blue": "0", + "blue_len": "0", + "green": "0", + "green_len": "0", + "orange": "0", + "orange_len": "0", + "linkrecover": "1", + "timeout": "1", + "state": False, + }, + 6: { + "cap": "2", + "max_rate": 1000, + "link_rate": 0, + "brown": "0", + "brown_len": "0", + "blue": "0", + "blue_len": "0", + "green": "0", + "green_len": "0", + "orange": "0", + "orange_len": "0", + "linkrecover": "1", + "timeout": "1", + "state": False, + }, + 7: { + "cap": "2", + "max_rate": 1000, + "link_rate": 0, + "brown": "0", + "brown_len": "0", + "blue": "0", + "blue_len": "0", + "green": "0", + "green_len": "0", + "orange": "0", + "orange_len": "0", + "linkrecover": "1", + "timeout": "1", + "state": False, + }, + 8: { + "cap": "2", + "max_rate": 1000, + "link_rate": 1000, + "brown": "0", + "brown_len": "0", + "blue": "0", + "blue_len": "0", + "green": "0", + "green_len": "0", + "orange": "0", + "orange_len": "0", + "linkrecover": "1", + "timeout": "1", + "state": True, + }, + }, + PortType.USB: { + 1: {"cap": "128", "max_rate": 5000, "link_rate": 0, "state": False}, + 2: {"cap": "128", "max_rate": 5000, "link_rate": 0, "state": False}, + }, + PortType.WAN: { + 0: { + "cap": "1073741825", + "max_rate": 1000, + "link_rate": 1000, + "brown": "0", + "brown_len": "0", + "blue": "0", + "blue_len": "0", + "green": "0", + "green_len": "0", + "orange": "0", + "orange_len": "0", + "linkrecover": "1", + "timeout": "1", + "state": True, + } + }, + }, +} diff --git a/tests/test_data/rt_ax88u_merlin_388/sysinfo_001.content b/tests/test_data/rt_ax88u_merlin_388/sysinfo_001.content new file mode 100644 index 0000000..77db475 --- /dev/null +++ b/tests/test_data/rt_ax88u_merlin_388/sysinfo_001.content @@ -0,0 +1,10 @@ +wlc_0_arr = ["11", "11", "11"]; +wlc_1_arr = ["3", "3", "3"]; +wlc_2_arr = ["0", "0", "0"]; +wlc_3_arr = ["0", "0", "0"]; +conn_stats_arr = ["320","86"]; +mem_stats_arr = ["882.34", "240.57", "0.00", +"52.73", "0.00", "0.00", +"85328", "7.49 / 63.00 MB"]; +cpu_stats_arr = ["1.98", "2.04", "2.01"]; + diff --git a/tests/test_data/rt_ax88u_merlin_388/sysinfo_001.py b/tests/test_data/rt_ax88u_merlin_388/sysinfo_001.py new file mode 100644 index 0000000..be03f8b --- /dev/null +++ b/tests/test_data/rt_ax88u_merlin_388/sysinfo_001.py @@ -0,0 +1,44 @@ +"""Result of processing sysinfo_001.content.""" + +from asusrouter import AsusData +from asusrouter.modules.wlan import Wlan + +expected_result = { + AsusData.SYSINFO: { + "wlan": { + Wlan.FREQ_2G: { + "client_associated": 11, + "client_authorized": 11, + "client_authenticated": 11, + }, + Wlan.FREQ_5G: { + "client_associated": 3, + "client_authorized": 3, + "client_authenticated": 3, + }, + Wlan.FREQ_5G2: { + "client_associated": 0, + "client_authorized": 0, + "client_authenticated": 0, + }, + Wlan.FREQ_6G: { + "client_associated": 0, + "client_authorized": 0, + "client_authenticated": 0, + }, + }, + "connections": {"total": 320, "active": 86}, + "memory": { + "total": 882.34, + "free": 240.57, + "buffers": 0.0, + "cache": 52.73, + "swap_1": 0.0, + "swap_2": 0.0, + "nvram": 85328, + "jffs_used": 7.49, + "jffs_total": 63.0, + }, + "load_avg": {1: 1.98, 5: 2.04, 15: 2.01}, + } +} diff --git a/tests/test_data/rt_ax88u_merlin_388/temperature_001.content b/tests/test_data/rt_ax88u_merlin_388/temperature_001.content new file mode 100644 index 0000000..4b70f3c --- /dev/null +++ b/tests/test_data/rt_ax88u_merlin_388/temperature_001.content @@ -0,0 +1,11 @@ +curr_coreTmp_wl0_raw = "44°C"; +curr_coreTmp_wl0 = (curr_coreTmp_wl0_raw.indexOf("disabled") > 0 ? 0 : curr_coreTmp_wl0_raw.replace("°C", "")); +curr_coreTmp_wl1_raw = "44°C"; +curr_coreTmp_wl1 = (curr_coreTmp_wl1_raw.indexOf("disabled") > 0 ? 0 : curr_coreTmp_wl1_raw.replace("°C", "")); +curr_coreTmp_wl2_raw = "disabled"; +curr_coreTmp_wl2 = (curr_coreTmp_wl2_raw.indexOf("disabled") > 0 ? 0 : curr_coreTmp_wl2_raw.replace("°C", "")); +curr_coreTmp_wl3_raw = "disabled"; +curr_coreTmp_wl3 = (curr_coreTmp_wl3_raw.indexOf("disabled") > 0 ? 0 : curr_coreTmp_wl3_raw.replace("°C", "")); +curr_cpuTemp = "65.514"; +fanctrl_info = ""; + diff --git a/tests/test_data/rt_ax88u_merlin_388/temperature_001.py b/tests/test_data/rt_ax88u_merlin_388/temperature_001.py new file mode 100644 index 0000000..227f02e --- /dev/null +++ b/tests/test_data/rt_ax88u_merlin_388/temperature_001.py @@ -0,0 +1,12 @@ +"""Result of processing firmware_001.content.""" + +from asusrouter import AsusData +from asusrouter.modules.wlan import Wlan + +expected_result = { + AsusData.TEMPERATURE: { + Wlan.FREQ_2G: 44.0, + Wlan.FREQ_5G: 44.0, + "cpu": 65.514, + } +} diff --git a/tests/test_data/rt_ax88u_merlin_388/update_clients_001.content b/tests/test_data/rt_ax88u_merlin_388/update_clients_001.content new file mode 100644 index 0000000..9f3d379 --- /dev/null +++ b/tests/test_data/rt_ax88u_merlin_388/update_clients_001.content @@ -0,0 +1,8 @@ +originDataTmp = originData; +originData = { +fromNetworkmapd : [{ "00:00:00:00:00:01": { "type": "0", "defaultType": "0", "name": "Espressif Inc ", "nickName": "FakeNickName", "ip": "192.168.1.147", "mac": "00:00:00:00:00:01", "from": "networkmapd", "macRepeat": "0", "isGateway": "0", "isWebServer": "0", "isPrinter": "0", "isITunes": "0", "dpiType": "", "dpiDevice": "", "vendor": "Espressif Inc.", "isWL": "1", "isGN": "", "isOnline": "1", "ssid": "", "isLogin": "0", "opMode": "0", "rssi": "-43", "curTx": "54", "curRx": "11", "totalTx": "", "totalRx": "", "wlConnectTime": "141:25:19", "ipMethod": "Manual", "ROG": "0", "group": "", "callback": "", "keeparp": "", "qosLevel": "", "wtfast": "0", "internetMode": "allow", "internetState": 1, "amesh_isReClient": "1", "amesh_papMac": "00:00:00:00:00:00", "amesh_bind_mac": "", "amesh_bind_band": "0" }, "00:00:00:00:00:02": { "type": "30", "defaultType": "30", "name": "MSFT 5 0", "nickName": "FakeNickName", "ip": "192.168.1.22", "mac": "00:00:00:00:00:02", "from": "networkmapd", "macRepeat": "0", "isGateway": "0", "isWebServer": "0", "isPrinter": "0", "isITunes": "0", "dpiType": "", "dpiDevice": "", "vendor": "MSFT 5.0", "isWL": "2", "isGN": "", "isOnline": "0", "ssid": "", "isLogin": "0", "opMode": "0", "rssi": "-25", "curTx": "2401.9", "curRx": "1225", "totalTx": "", "totalRx": "", "wlConnectTime": "00:00:33", "ipMethod": "Manual", "ROG": "0", "group": "", "callback": "", "keeparp": "", "qosLevel": "", "wtfast": "0", "internetMode": "allow", "internetState": "1", "amesh_bind_mac": "", "amesh_bind_band": "0" }, "00:00:00:00:00:03": { "type": "30", "defaultType": "30", "name": "FakeName", "nickName": "FakeNickName", "ip": "192.168.1.10", "mac": "00:00:00:00:00:03", "from": "networkmapd", "macRepeat": "0", "isGateway": "0", "isWebServer": "0", "isPrinter": "0", "isITunes": "0", "dpiType": "", "dpiDevice": "", "vendor": "MSFT 5.0", "isWL": "0", "isGN": "", "isOnline": "1", "ssid": "", "isLogin": "0", "opMode": "0", "rssi": "0", "curTx": "", "curRx": "", "totalTx": "", "totalRx": "", "wlConnectTime": "", "ipMethod": "Manual", "ROG": "0", "group": "", "callback": "", "keeparp": "", "qosLevel": "", "wtfast": "0", "internetMode": "allow", "internetState": "1", "amesh_isReClient": "1", "amesh_papMac": "00:00:00:00:00:00", "amesh_bind_mac": "", "amesh_bind_band": "0" }, "00:00:00:00:00:04": { "type": "0", "defaultType": "0", "name": "FakeName", "nickName": "FakeNickName", "ip": "192.168.1.112", "mac": "00:00:00:00:00:04", "from": "networkmapd", "macRepeat": "0", "isGateway": "0", "isWebServer": "0", "isPrinter": "0", "isITunes": "0", "dpiType": "", "dpiDevice": "", "vendor": "Espressif Inc.", "isWL": "1", "isGN": "", "isOnline": "1", "ssid": "", "isLogin": "0", "opMode": "0", "rssi": "-50", "curTx": "57.8", "curRx": "6", "totalTx": "", "totalRx": "", "wlConnectTime": "32:24:36", "ipMethod": "Manual", "ROG": "0", "group": "", "callback": "", "keeparp": "", "qosLevel": "", "wtfast": "0", "internetMode": "block", "internetState": 0, "amesh_isReClient": "1", "amesh_papMac": "00:00:00:00:00:00", "amesh_bind_mac": "", "amesh_bind_band": "0" }, "00:00:00:00:00:05": { "type": "0", "defaultType": "0", "name": "FakeName", "nickName": "FakeNickName", "ip": "192.168.1.12", "mac": "00:00:00:00:00:05", "from": "networkmapd", "macRepeat": "0", "isGateway": "0", "isWebServer": "0", "isPrinter": "0", "isITunes": "0", "dpiType": "", "dpiDevice": "", "vendor": "Microsoft Corporation", "isWL": "0", "isGN": "", "isOnline": "1", "ssid": "", "isLogin": "0", "opMode": "0", "rssi": "0", "curTx": "", "curRx": "", "totalTx": "", "totalRx": "", "wlConnectTime": "", "ipMethod": "Manual", "ROG": "0", "group": "", "callback": "", "keeparp": "", "qosLevel": "", "wtfast": "0", "internetMode": "allow", "internetState": "1", "amesh_isReClient": "1", "amesh_papMac": "00:00:00:00:00:00", "amesh_bind_mac": "", "amesh_bind_band": "0" }, "00:00:00:00:00:06": { "type": "6", "defaultType": "6", "name": "FakeName", "nickName": "FakeNickName", "ip": "192.168.1.102", "mac": "00:00:00:00:00:06", "from": "networkmapd", "macRepeat": "0", "isGateway": "0", "isWebServer": "0", "isPrinter": "0", "isITunes": "0", "dpiType": "", "dpiDevice": "", "vendor": "AzureWave Technology Inc.", "isWL": "1", "isGN": "", "isOnline": "1", "ssid": "", "isLogin": "0", "opMode": "0", "rssi": "-44", "curTx": "58.5", "curRx": "72.2", "totalTx": "", "totalRx": "", "wlConnectTime": "141:25:19", "ipMethod": "Manual", "ROG": "0", "group": "", "callback": "", "keeparp": "", "qosLevel": "", "wtfast": "0", "internetMode": "allow", "internetState": "1", "amesh_isReClient": "1", "amesh_papMac": "00:00:00:00:00:00", "amesh_bind_mac": "", "amesh_bind_band": "0" }, "00:00:00:00:00:07": { "type": "0", "defaultType": "0", "name": "FakeName", "nickName": "FakeNickName", "ip": "192.168.1.73", "mac": "00:00:00:00:00:07", "from": "networkmapd", "macRepeat": "0", "isGateway": "0", "isWebServer": "0", "isPrinter": "0", "isITunes": "0", "dpiType": "", "dpiDevice": "", "vendor": "", "isWL": "1", "isGN": "", "isOnline": "1", "ssid": "", "isLogin": "0", "opMode": "0", "rssi": "-34", "curTx": "72.2", "curRx": "6", "totalTx": "", "totalRx": "", "wlConnectTime": "08:25:35", "ipMethod": "Manual", "ROG": "0", "group": "", "callback": "", "keeparp": "", "qosLevel": "", "wtfast": "0", "internetMode": "allow", "internetState": "1", "amesh_isReClient": "1", "amesh_papMac": "00:00:00:00:00:00", "amesh_bind_mac": "", "amesh_bind_band": "0" }, "00:00:00:00:00:08": { "type": "0", "defaultType": "0", "name": "Espressif Inc ", "nickName": "FakeNickName", "ip": "192.168.1.116", "mac": "00:00:00:00:00:08", "from": "networkmapd", "macRepeat": "0", "isGateway": "0", "isWebServer": "0", "isPrinter": "0", "isITunes": "0", "dpiType": "", "dpiDevice": "", "vendor": "Espressif Inc.", "isWL": "1", "isGN": "", "isOnline": "1", "ssid": "", "isLogin": "0", "opMode": "0", "rssi": "-26", "curTx": "65", "curRx": "54", "totalTx": "", "totalRx": "", "wlConnectTime": "32:24:41", "ipMethod": "Manual", "ROG": "0", "group": "", "callback": "", "keeparp": "", "qosLevel": "", "wtfast": "0", "internetMode": "block", "internetState": 0, "amesh_isReClient": "1", "amesh_papMac": "00:00:00:00:00:00", "amesh_bind_mac": "", "amesh_bind_band": "0" }, "00:00:00:00:00:09": { "type": "0", "defaultType": "0", "name": "FakeName", "nickName": "FakeNickName", "ip": "192.168.1.146", "mac": "00:00:00:00:00:09", "from": "networkmapd", "macRepeat": "0", "isGateway": "0", "isWebServer": "0", "isPrinter": "0", "isITunes": "0", "dpiType": "", "dpiDevice": "", "vendor": "Espressif Inc.", "isWL": "1", "isGN": "", "isOnline": "1", "ssid": "", "isLogin": "0", "opMode": "0", "rssi": "-53", "curTx": "54", "curRx": "11", "totalTx": "", "totalRx": "", "wlConnectTime": "141:25:19", "ipMethod": "Manual", "ROG": "0", "group": "", "callback": "", "keeparp": "", "qosLevel": "", "wtfast": "0", "internetMode": "allow", "internetState": "1", "amesh_isReClient": "1", "amesh_papMac": "00:00:00:00:00:00", "amesh_bind_mac": "", "amesh_bind_band": "0" }, "00:00:00:00:00:10": { "type": "30", "defaultType": "30", "name": "FakeName", "nickName": "FakeNickName", "ip": "192.168.1.21", "mac": "00:00:00:00:00:10", "from": "networkmapd", "macRepeat": "0", "isGateway": "0", "isWebServer": "0", "isPrinter": "0", "isITunes": "0", "dpiType": "", "dpiDevice": "", "vendor": "MSFT 5.0", "isWL": "0", "isGN": "", "isOnline": "1", "ssid": "", "isLogin": "1", "opMode": "0", "rssi": "0", "curTx": "", "curRx": "", "totalTx": "", "totalRx": "", "wlConnectTime": "", "ipMethod": "Manual", "ROG": "0", "group": "", "callback": "", "keeparp": "", "qosLevel": "", "wtfast": "0", "internetMode": "allow", "internetState": "1", "amesh_isReClient": "1", "amesh_papMac": "00:00:00:00:00:00", "amesh_bind_mac": "", "amesh_bind_band": "0" }, "00:00:00:00:00:11": { "type": "0", "defaultType": "0", "name": "FakeName", "nickName": "FakeNickName", "ip": "192.168.1.11", "mac": "00:00:00:00:00:11", "from": "networkmapd", "macRepeat": "0", "isGateway": "0", "isWebServer": "0", "isPrinter": "0", "isITunes": "0", "dpiType": "", "dpiDevice": "", "vendor": "Microsoft Corporation", "isWL": "0", "isGN": "", "isOnline": "1", "ssid": "", "isLogin": "0", "opMode": "0", "rssi": "0", "curTx": "", "curRx": "", "totalTx": "", "totalRx": "", "wlConnectTime": "", "ipMethod": "Manual", "ROG": "0", "group": "", "callback": "", "keeparp": "", "qosLevel": "", "wtfast": "0", "internetMode": "allow", "internetState": "1", "amesh_isReClient": "1", "amesh_papMac": "00:00:00:00:00:00", "amesh_bind_mac": "", "amesh_bind_band": "0" }, "00:00:00:00:00:12": { "type": "0", "defaultType": "0", "name": "Espressif Inc ", "nickName": "FakeNickName", "ip": "192.168.1.113", "mac": "00:00:00:00:00:12", "from": "networkmapd", "macRepeat": "0", "isGateway": "0", "isWebServer": "0", "isPrinter": "0", "isITunes": "0", "dpiType": "", "dpiDevice": "", "vendor": "Espressif Inc.", "isWL": "1", "isGN": "", "isOnline": "1", "ssid": "", "isLogin": "0", "opMode": "0", "rssi": "-58", "curTx": "65", "curRx": "54", "totalTx": "", "totalRx": "", "wlConnectTime": "32:24:35", "ipMethod": "Manual", "ROG": "0", "group": "", "callback": "", "keeparp": "", "qosLevel": "", "wtfast": "0", "internetMode": "block", "internetState": 0, "amesh_isReClient": "1", "amesh_papMac": "00:00:00:00:00:00", "amesh_bind_mac": "", "amesh_bind_band": "0" }, "00:00:00:00:00:13": { "type": "0", "defaultType": "0", "name": "FakeName", "nickName": "FakeNickName", "ip": "192.168.1.101", "mac": "00:00:00:00:00:13", "from": "networkmapd", "macRepeat": "0", "isGateway": "0", "isWebServer": "0", "isPrinter": "0", "isITunes": "0", "dpiType": "", "dpiDevice": "", "vendor": "Seiko Epson Corporation", "isWL": "1", "isGN": "", "isOnline": "1", "ssid": "", "isLogin": "0", "opMode": "0", "rssi": "-41", "curTx": "130", "curRx": "1", "totalTx": "", "totalRx": "", "wlConnectTime": "141:25:16", "ipMethod": "Manual", "ROG": "0", "group": "", "callback": "", "keeparp": "", "qosLevel": "", "wtfast": "0", "internetMode": "allow", "internetState": "1", "amesh_isReClient": "1", "amesh_papMac": "00:00:00:00:00:00", "amesh_bind_mac": "", "amesh_bind_band": "0" }, "00:00:00:00:00:14": { "type": "0", "defaultType": "0", "name": "FakeName", "nickName": "FakeNickName", "ip": "192.168.1.115", "mac": "00:00:00:00:00:14", "from": "networkmapd", "macRepeat": "0", "isGateway": "0", "isWebServer": "0", "isPrinter": "0", "isITunes": "0", "dpiType": "", "dpiDevice": "", "vendor": "Espressif Inc.", "isWL": "1", "isGN": "", "isOnline": "1", "ssid": "", "isLogin": "0", "opMode": "0", "rssi": "-41", "curTx": "54", "curRx": "54", "totalTx": "", "totalRx": "", "wlConnectTime": "116:41:53", "ipMethod": "Manual", "ROG": "0", "group": "", "callback": "", "keeparp": "", "qosLevel": "", "wtfast": "0", "internetMode": "block", "internetState": 0, "amesh_isReClient": "1", "amesh_papMac": "00:00:00:00:00:00", "amesh_bind_mac": "", "amesh_bind_band": "0" }, "00:00:00:00:00:15": { "type": "5", "defaultType": "4", "name": "FakeName", "nickName": "FakeNickName", "ip": "192.168.1.15", "mac": "00:00:00:00:00:15", "from": "networkmapd", "macRepeat": "0", "isGateway": "0", "isWebServer": "0", "isPrinter": "0", "isITunes": "0", "dpiType": "", "dpiDevice": "", "vendor": "Synology Incorporated", "isWL": "0", "isGN": "", "isOnline": "1", "ssid": "", "isLogin": "0", "opMode": "0", "rssi": "0", "curTx": "", "curRx": "", "totalTx": "", "totalRx": "", "wlConnectTime": "", "ipMethod": "Manual", "ROG": "0", "group": "", "callback": "", "keeparp": "", "qosLevel": "", "wtfast": "0", "internetMode": "allow", "internetState": "1", "amesh_isReClient": "1", "amesh_papMac": "00:00:00:00:00:00", "amesh_bind_mac": "", "amesh_bind_band": "0" }, "00:00:00:00:00:16": { "type": "10", "defaultType": "0", "name": "FakeName", "nickName": "FakeNickName", "ip": "192.168.1.25", "mac": "00:00:00:00:00:16", "from": "networkmapd", "macRepeat": "0", "isGateway": "0", "isWebServer": "0", "isPrinter": "0", "isITunes": "0", "dpiType": "", "dpiDevice": "", "vendor": "", "isWL": "2", "isGN": "", "isOnline": "1", "ssid": "", "isLogin": "0", "opMode": "0", "rssi": "-40", "curTx": "2041.7", "curRx": "24", "totalTx": "", "totalRx": "", "wlConnectTime": "13:39:08", "ipMethod": "DHCP", "ROG": "0", "group": "", "callback": "", "keeparp": "", "qosLevel": "", "wtfast": "0", "internetMode": "allow", "internetState": "1", "amesh_isReClient": "1", "amesh_papMac": "00:00:00:00:00:00", "amesh_bind_mac": "", "amesh_bind_band": "0" }, "00:00:00:00:00:17": { "type": "0", "defaultType": "0", "name": "FakeName", "nickName": "FakeNickName", "ip": "192.168.1.114", "mac": "00:00:00:00:00:17", "from": "networkmapd", "macRepeat": "0", "isGateway": "0", "isWebServer": "0", "isPrinter": "0", "isITunes": "0", "dpiType": "", "dpiDevice": "", "vendor": "Espressif Inc.", "isWL": "1", "isGN": "", "isOnline": "1", "ssid": "", "isLogin": "0", "opMode": "0", "rssi": "-52", "curTx": "54", "curRx": "6", "totalTx": "", "totalRx": "", "wlConnectTime": "111:46:30", "ipMethod": "Manual", "ROG": "0", "group": "", "callback": "", "keeparp": "", "qosLevel": "", "wtfast": "0", "internetMode": "block", "internetState": 0, "amesh_isReClient": "1", "amesh_papMac": "00:00:00:00:00:00", "amesh_bind_mac": "", "amesh_bind_band": "0" }, "00:00:00:00:00:18": { "type": "0", "defaultType": "0", "name": "FakeName", "nickName": "FakeNickName", "ip": "192.168.1.52", "mac": "00:00:00:00:00:18", "from": "networkmapd", "macRepeat": "0", "isGateway": "0", "isWebServer": "0", "isPrinter": "0", "isITunes": "0", "dpiType": "", "dpiDevice": "", "vendor": "Apple, Inc.", "isWL": "2", "isGN": "", "isOnline": "1", "ssid": "", "isLogin": "0", "opMode": "0", "rssi": "-43", "curTx": "1134.2", "curRx": "24", "totalTx": "", "totalRx": "", "wlConnectTime": "05:18:53", "ipMethod": "Manual", "ROG": "0", "group": "", "callback": "", "keeparp": "", "qosLevel": "", "wtfast": "0", "internetMode": "allow", "internetState": "1", "amesh_isReClient": "1", "amesh_papMac": "00:00:00:00:00:00", "amesh_bind_mac": "", "amesh_bind_band": "0" }, "00:00:00:00:00:19": { "type": "31", "defaultType": "9", "name": "FakeName", "nickName": "FakeNickName", "ip": "192.168.1.24", "mac": "00:00:00:00:00:19", "from": "networkmapd", "macRepeat": "0", "isGateway": "0", "isWebServer": "0", "isPrinter": "0", "isITunes": "0", "dpiType": "", "dpiDevice": "", "vendor": "android-dhcp-13", "isWL": "2", "isGN": "", "isOnline": "1", "ssid": "", "isLogin": "0", "opMode": "0", "rssi": "-27", "curTx": "1134.2", "curRx": "6", "totalTx": "", "totalRx": "", "wlConnectTime": "08:27:14", "ipMethod": "Manual", "ROG": "0", "group": "", "callback": "", "keeparp": "", "qosLevel": "", "wtfast": "0", "internetMode": "allow", "internetState": "1", "amesh_isReClient": "1", "amesh_papMac": "00:00:00:00:00:00", "amesh_bind_mac": "", "amesh_bind_band": "0" }, "00:00:00:00:00:20": { "type": "0", "defaultType": "0", "name": "FakeName", "nickName": "FakeNickName", "ip": "192.168.1.111", "mac": "00:00:00:00:00:20", "from": "networkmapd", "macRepeat": "0", "isGateway": "0", "isWebServer": "0", "isPrinter": "0", "isITunes": "0", "dpiType": "", "dpiDevice": "", "vendor": "Espressif Inc.", "isWL": "1", "isGN": "", "isOnline": "1", "ssid": "", "isLogin": "0", "opMode": "0", "rssi": "-47", "curTx": "72.2", "curRx": "6", "totalTx": "", "totalRx": "", "wlConnectTime": "32:01:57", "ipMethod": "Manual", "ROG": "0", "group": "", "callback": "", "keeparp": "", "qosLevel": "", "wtfast": "0", "internetMode": "block", "internetState": 0, "amesh_isReClient": "1", "amesh_papMac": "00:00:00:00:00:00", "amesh_bind_mac": "", "amesh_bind_band": "0" }, "00:00:00:00:00:29": { "type": "31", "defaultType": "9", "name": "FakeName", "nickName": "FakeNickName", "ip": "192.168.1.51", "mac": "00:00:00:00:00:29", "from": "networkmapd", "macRepeat": "0", "isGateway": "0", "isWebServer": "0", "isPrinter": "0", "isITunes": "0", "dpiType": "", "dpiDevice": "", "vendor": "android-dhcp-10", "isWL": "2", "isGN": "", "isOnline": "0", "ssid": "", "isLogin": "0", "opMode": "0", "rssi": "-46", "curTx": "780", "curRx": "975", "totalTx": "", "totalRx": "", "wlConnectTime": "24:22:53", "ipMethod": "Manual", "ROG": "0", "group": "", "callback": "", "keeparp": "", "qosLevel": "", "wtfast": "0", "internetMode": "allow", "internetState": "1", "amesh_bind_mac": "", "amesh_bind_band": "0" }, "00:00:00:00:00:58": { "type": "34", "defaultType": "7", "name": "FakeName", "nickName": "FakeNickName", "ip": "192.168.1.103", "mac": "00:00:00:00:00:58", "from": "networkmapd", "macRepeat": "0", "isGateway": "0", "isWebServer": "0", "isPrinter": "0", "isITunes": "0", "dpiType": "", "dpiDevice": "", "vendor": "MSFT 5.0 XBOX", "isWL": "0", "isGN": "", "isOnline": "0", "ssid": "", "isLogin": "0", "opMode": "0", "rssi": "0", "curTx": "", "curRx": "", "totalTx": "", "totalRx": "", "wlConnectTime": "", "ipMethod": "Manual", "ROG": "0", "group": "", "callback": "", "keeparp": "", "qosLevel": "", "wtfast": "0", "internetMode": "allow", "internetState": "1", "amesh_bind_mac": "", "amesh_bind_band": "0" }, "00:00:00:00:00:33": { "type": "0", "defaultType": "0", "name": "FakeName", "nickName": "", "ip": "192.168.1.131", "mac": "00:00:00:00:00:33", "from": "networkmapd", "macRepeat": "0", "isGateway": "0", "isWebServer": "0", "isPrinter": "0", "isITunes": "0", "dpiType": "", "dpiDevice": "", "vendor": "Samsung Electronics Co., Ltd.", "isWL": "1", "isGN": "", "isOnline": "0", "ssid": "", "isLogin": "0", "opMode": "0", "rssi": "-49", "curTx": "65", "curRx": "65", "totalTx": "", "totalRx": "", "wlConnectTime": "00:00:42", "ipMethod": "DHCP", "ROG": "0", "group": "", "callback": "", "keeparp": "", "qosLevel": "", "wtfast": "0", "internetMode": "allow", "internetState": "1", "amesh_bind_mac": "", "amesh_bind_band": "0" }, "maclist": [ "00:00:00:00:00:01", "00:00:00:00:00:03", "00:00:00:00:00:04", "00:00:00:00:00:05", "00:00:00:00:00:06", "00:00:00:00:00:07", "00:00:00:00:00:08", "00:00:00:00:00:09", "00:00:00:00:00:10", "00:00:00:00:00:11", "00:00:00:00:00:12", "00:00:00:00:00:13", "00:00:00:00:00:14", "00:00:00:00:00:15", "00:00:00:00:00:16", "00:00:00:00:00:17", "00:00:00:00:00:18", "00:00:00:00:00:19", "00:00:00:00:00:20" ], "ClientAPILevel": "5" }], +nmpClient : [{ "00:00:00:00:00:10": { "mac": "00:00:00:00:00:10", "name": "FakeName", "vendor": "MSFT 5.0", "vendorclass": "MSFT 5.0", "os_type": 2, "type": "0", "nickName": "FakeNickName", "defaultType": "30", "from": "nmpClient", "ROG": "0", "amesh_isRe": "0", "amesh_bind_mac": "", "amesh_bind_band": "0" }, "00:00:00:00:00:17": { "mac": "00:00:00:00:00:17", "name": "FakeName", "vendor": "Espressif Inc.", "vendorclass": "Espressif Inc.", "os_type": 0, "type": "0", "nickName": "FakeNickName", "defaultType": "0", "from": "nmpClient", "ROG": "0", "amesh_isRe": "0", "amesh_bind_mac": "", "amesh_bind_band": "0" }, "00:00:00:00:00:11": { "mac": "00:00:00:00:00:11", "name": "FakeName", "vendor": "Microsoft Corporation", "vendorclass": "Microsoft", "os_type": 2, "type": "0", "nickName": "FakeNickName", "defaultType": "0", "from": "nmpClient", "ROG": "0", "amesh_isRe": "0", "amesh_bind_mac": "", "amesh_bind_band": "0" }, "00:00:00:00:00:21": { "mac": "00:00:00:00:00:21", "name": "FakeName", "vendor": "TP-LINK", "vendorclass": "", "os_type": 0, "type": "0", "nickName": "FakeNickName", "defaultType": "0", "from": "nmpClient", "ROG": "0", "amesh_isRe": "0", "amesh_bind_mac": "", "amesh_bind_band": "0" }, "00:00:00:00:00:05": { "mac": "00:00:00:00:00:05", "name": "FakeName", "vendor": "Microsoft Corporation", "vendorclass": "Microsoft", "os_type": 2, "type": "0", "nickName": "FakeNickName", "defaultType": "0", "from": "nmpClient", "ROG": "0", "amesh_isRe": "0", "amesh_bind_mac": "", "amesh_bind_band": "0" }, "00:00:00:00:00:03": { "mac": "00:00:00:00:00:03", "name": "FakeName", "vendor": "MSFT 5.0", "vendorclass": "MSFT 5.0", "os_type": 2, "type": "30", "nickName": "FakeNickName", "defaultType": "30", "from": "nmpClient", "ROG": "0", "amesh_isRe": "0", "amesh_bind_mac": "", "amesh_bind_band": "0" }, "00:00:00:00:00:22": { "mac": "00:00:00:00:00:22", "name": "", "vendor": "", "vendorclass": "", "os_type": 0, "type": "0", "nickName": "", "defaultType": "0", "from": "nmpClient", "ROG": "0", "amesh_isRe": "0", "amesh_bind_mac": "", "amesh_bind_band": "0" }, "00:00:00:00:00:20": { "mac": "00:00:00:00:00:20", "name": "FakeName", "vendor": "Espressif Inc.", "vendorclass": "Espressif Inc.", "os_type": 0, "type": "0", "nickName": "FakeNickName", "defaultType": "0", "from": "nmpClient", "ROG": "0", "amesh_isRe": "0", "amesh_bind_mac": "", "amesh_bind_band": "0" }, "00:00:00:00:00:23": { "mac": "00:00:00:00:00:23", "name": "FakeName", "vendor": "Asus", "vendorclass": "udhcp 1.25.1", "os_type": 0, "type": "24", "nickName": "FakeNickName", "defaultType": "2", "from": "nmpClient", "ROG": "0", "amesh_isRe": "0", "amesh_bind_mac": "", "amesh_bind_band": "0" }, "00:00:00:00:00:19": { "mac": "00:00:00:00:00:19", "name": "FakeName", "vendor": "android-dhcp-13", "vendorclass": "android-dhcp-13", "os_type": 1, "type": "31", "nickName": "FakeNickName", "defaultType": "9", "from": "nmpClient", "ROG": "0", "amesh_isRe": "0", "amesh_bind_mac": "", "amesh_bind_band": "0" }, "00:00:00:00:00:24": { "mac": "00:00:00:00:00:24", "name": "FakeName", "vendor": "Amazon", "vendorclass": "dhcpcd-6.8.2:Linux-4.9.77+:armv", "os_type": 3, "type": "22", "nickName": "FakeNickName", "defaultType": "22", "from": "nmpClient", "ROG": "0", "amesh_isRe": "0", "amesh_bind_mac": "", "amesh_bind_band": "0" }, "00:00:00:00:00:25": { "mac": "00:00:00:00:00:25", "name": "FakeName", "vendor": "Hangzhou BroadLink Technology Co., Ltd.", "vendorclass": "Hangzhou BroadLink Technology C", "os_type": 0, "type": "0", "nickName": "FakeNickName", "defaultType": "0", "from": "nmpClient", "ROG": "0", "amesh_isRe": "0", "amesh_bind_mac": "", "amesh_bind_band": "0" }, "00:00:00:00:00:12": { "mac": "00:00:00:00:00:12", "name": "Espressif Inc.", "vendor": "Espressif Inc.", "vendorclass": "Espressif Inc.", "os_type": 0, "type": "0", "nickName": "FakeNickName", "defaultType": "0", "from": "nmpClient", "ROG": "0", "amesh_isRe": "0", "amesh_bind_mac": "", "amesh_bind_band": "0" }, "00:00:00:00:00:26": { "mac": "00:00:00:00:00:26", "name": "Espressif Inc.", "vendor": "Espressif Inc.", "vendorclass": "", "os_type": 0, "type": "0", "nickName": "FakeNickName", "defaultType": "0", "from": "nmpClient", "ROG": "0", "amesh_isRe": "0", "amesh_bind_mac": "", "amesh_bind_band": "0" }, "00:00:00:00:00:06": { "mac": "00:00:00:00:00:06", "name": "FakeName", "vendor": "AzureWave Technology Inc.", "vendorclass": "AzureWave Technology Inc.", "os_type": 5, "type": "0", "nickName": "FakeNickName", "defaultType": "6", "from": "nmpClient", "ROG": "0", "amesh_isRe": "0", "amesh_bind_mac": "", "amesh_bind_band": "0" }, "00:00:00:00:00:08": { "mac": "00:00:00:00:00:08", "name": "Espressif Inc.", "vendor": "Espressif Inc.", "vendorclass": "Espressif Inc.", "os_type": 0, "type": "0", "nickName": "FakeNickName", "defaultType": "0", "from": "nmpClient", "ROG": "0", "amesh_isRe": "0", "amesh_bind_mac": "", "amesh_bind_band": "0" }, "00:00:00:00:00:13": { "mac": "00:00:00:00:00:13", "name": "FakeName", "vendor": "Seiko Epson Corporation", "vendorclass": "Seiko Epson Corporation", "os_type": 0, "type": "0", "nickName": "FakeNickName", "defaultType": "0", "from": "nmpClient", "ROG": "0", "amesh_isRe": "0", "amesh_bind_mac": "", "amesh_bind_band": "0" }, "00:00:00:00:00:27": { "mac": "00:00:00:00:00:27", "name": "FakeName", "vendor": "Espressif Inc.", "vendorclass": "", "os_type": 0, "type": "0", "nickName": "FakeNickName", "defaultType": "0", "from": "nmpClient", "ROG": "0", "amesh_isRe": "0", "amesh_bind_mac": "", "amesh_bind_band": "0" }, "00:00:00:00:00:14": { "mac": "00:00:00:00:00:14", "name": "FakeName", "vendor": "Espressif Inc.", "type": "0", "vendorclass": "Espressif Inc.", "os_type": 0, "nickName": "FakeNickName", "defaultType": "0", "from": "nmpClient", "ROG": "0", "amesh_isRe": "0", "amesh_bind_mac": "", "amesh_bind_band": "0" }, "00:00:00:00:00:28": { "mac": "00:00:00:00:00:28", "name": "FakeName", "vendor": "Intel Corporate", "vendorclass": "MSFT 5.0", "os_type": 2, "type": "30", "nickName": "FakeNickName", "defaultType": "30", "from": "nmpClient", "ROG": "0", "amesh_isRe": "0", "amesh_bind_mac": "", "amesh_bind_band": "0" }, "00:00:00:00:00:04": { "mac": "00:00:00:00:00:04", "name": "FakeName", "vendor": "Espressif Inc.", "type": "0", "vendorclass": "Espressif Inc.", "os_type": 0, "nickName": "FakeNickName", "defaultType": "0", "from": "nmpClient", "ROG": "0", "amesh_isRe": "0", "amesh_bind_mac": "", "amesh_bind_band": "0" }, "00:00:00:00:00:29": { "mac": "00:00:00:00:00:29", "name": "FakeName", "vendor": "android-dhcp-10", "type": "31", "vendorclass": "android-dhcp-10", "os_type": 1, "nickName": "FakeNickName", "defaultType": "9", "from": "nmpClient", "ROG": "0", "amesh_isRe": "0", "amesh_bind_mac": "", "amesh_bind_band": "0" }, "00:00:00:00:00:30": { "mac": "00:00:00:00:00:30", "name": "FakeName", "vendor": "android-dhcp-10", "vendorclass": "android-dhcp-10", "os_type": 1, "type": "31", "nickName": "FakeNickName", "defaultType": "9", "from": "nmpClient", "ROG": "0", "amesh_isRe": "0", "amesh_bind_mac": "", "amesh_bind_band": "0" }, "00:00:00:00:00:18": { "mac": "00:00:00:00:00:18", "name": "FakeName", "vendor": "Apple, Inc.", "vendorclass": "Apple", "os_type": 5, "type": "0", "nickName": "FakeNickName", "defaultType": "0", "from": "nmpClient", "ROG": "0", "amesh_isRe": "0", "amesh_bind_mac": "", "amesh_bind_band": "0" }, "00:00:00:00:00:31": { "mac": "00:00:00:00:00:31", "name": "FakeName", "vendor": "", "type": "30", "vendorclass": "MSFT 5.0", "os_type": 2, "nickName": "00:00:00:00:00:31", "defaultType": "30", "from": "nmpClient", "ROG": "0", "amesh_isRe": "0", "amesh_bind_mac": "", "amesh_bind_band": "0" }, "00:00:00:00:00:32": { "mac": "00:00:00:00:00:32", "name": "Espressif Inc.", "vendor": "Espressif Inc.", "vendorclass": "", "os_type": 0, "type": "0", "nickName": "FakeNickName", "defaultType": "0", "from": "nmpClient", "ROG": "0", "amesh_isRe": "0", "amesh_bind_mac": "", "amesh_bind_band": "0" }, "00:00:00:00:00:02": { "mac": "00:00:00:00:00:02", "name": "MSFT 5.0", "vendor": "MSFT 5.0", "type": "30", "vendorclass": "MSFT 5.0", "os_type": 2, "nickName": "FakeNickName", "defaultType": "30", "from": "nmpClient", "ROG": "0", "amesh_isRe": "0", "amesh_bind_mac": "", "amesh_bind_band": "0" }, "00:00:00:00:00:33": { "mac": "00:00:00:00:00:33", "name": "FakeName", "vendor": "Samsung Electronics Co., Ltd.", "vendorclass": "Samsung", "os_type": 0, "type": "0", "nickName": "", "defaultType": "0", "from": "nmpClient", "ROG": "0", "amesh_isRe": "0", "amesh_bind_mac": "", "amesh_bind_band": "0" }, "00:00:00:00:00:34": { "mac": "00:00:00:00:00:34", "name": "FakeName", "vendor": "Samsung Electronics Co., Ltd.", "vendorclass": "Samsung", "os_type": 0, "type": "0", "nickName": "FakeNickName", "defaultType": "0", "from": "nmpClient", "ROG": "0", "amesh_isRe": "0", "amesh_bind_mac": "", "amesh_bind_band": "0" }, "00:00:00:00:00:35": { "mac": "00:00:00:00:00:35", "name": "Xiaomi Communications Co Ltd", "vendor": "Xiaomi Communications Co Ltd", "vendorclass": "android-dhcp-9", "os_type": 1, "type": "31", "nickName": "", "defaultType": "31", "from": "nmpClient", "ROG": "0", "amesh_isRe": "0", "amesh_bind_mac": "", "amesh_bind_band": "0" }, "00:00:00:00:00:44": { "mac": "00:00:00:00:00:44", "name": "dhcpcd-5.5.6", "vendor": "dhcpcd-5.5.6", "vendorclass": "dhcpcd-5.5.6", "os_type": 3, "type": "0", "nickName": "FakeNickName", "defaultType": "22", "from": "nmpClient", "ROG": "0", "amesh_isRe": "0", "amesh_bind_mac": "", "amesh_bind_band": "0" }, "00:00:00:00:00:36": { "mac": "00:00:00:00:00:36", "name": "FakeName", "vendor": "", "type": "0", "vendorclass": "android-dhcp-10", "os_type": 1, "nickName": "FakeNickName", "defaultType": "31", "from": "nmpClient", "ROG": "0", "amesh_isRe": "0", "amesh_bind_mac": "", "amesh_bind_band": "0" }, "00:00:00:00:00:37": { "mac": "00:00:00:00:00:37", "name": "FakeName", "vendor": "", "vendorclass": "android-dhcp-12", "os_type": 1, "type": "31", "nickName": "", "defaultType": "31", "from": "nmpClient", "ROG": "0", "amesh_isRe": "0", "amesh_bind_mac": "", "amesh_bind_band": "0" }, "00:00:00:00:00:38": { "mac": "00:00:00:00:00:38", "name": "", "vendor": "", "vendorclass": "", "os_type": 0, "type": "0", "nickName": "", "defaultType": "0", "from": "nmpClient", "ROG": "0", "amesh_isRe": "0", "amesh_bind_mac": "", "amesh_bind_band": "0" }, "00:00:00:00:00:39": { "mac": "00:00:00:00:00:39", "name": "Intel Corporate", "vendor": "Intel Corporate", "vendorclass": "MSFT 5.0", "os_type": 2, "type": "30", "nickName": "FakeNickName", "defaultType": "30", "from": "nmpClient", "ROG": "0", "amesh_isRe": "0", "amesh_bind_mac": "", "amesh_bind_band": "0" }, "00:00:00:00:00:40": { "mac": "00:00:00:00:00:40", "name": "FakeName", "vendor": "dhcpcd-6.6.6:Linux-3.0.35+:armv", "vendorclass": "dhcpcd-6.6.6:Linux-3.0.35+:armv", "os_type": 3, "type": "22", "nickName": "", "defaultType": "22", "from": "nmpClient", "ROG": "0", "amesh_isRe": "0", "amesh_bind_mac": "", "amesh_bind_band": "0" }, "00:00:00:00:00:41": { "mac": "00:00:00:00:00:41", "name": "MSFT 5.0", "vendor": "MSFT 5.0", "vendorclass": "MSFT 5.0", "os_type": 2, "type": "30", "nickName": "FakeNickName", "defaultType": "30", "from": "nmpClient", "ROG": "0", "amesh_isRe": "0", "amesh_bind_mac": "", "amesh_bind_band": "0" }, "00:00:00:00:00:01": { "mac": "00:00:00:00:00:01", "name": "Espressif Inc.", "vendor": "Espressif Inc.", "vendorclass": "Espressif Inc.", "os_type": 0, "type": "0", "nickName": "FakeNickName", "defaultType": "0", "from": "nmpClient", "ROG": "0", "amesh_isRe": "0", "amesh_bind_mac": "", "amesh_bind_band": "0" }, "00:00:00:00:00:09": { "mac": "00:00:00:00:00:09", "name": "FakeName", "vendor": "Espressif Inc.", "vendorclass": "Espressif Inc.", "os_type": 0, "type": "0", "nickName": "FakeNickName", "defaultType": "0", "from": "nmpClient", "ROG": "0", "amesh_isRe": "0", "amesh_bind_mac": "", "amesh_bind_band": "0" }, "00:00:00:00:00:42": { "mac": "00:00:00:00:00:42", "name": "Espressif Inc.", "vendor": "Espressif Inc.", "vendorclass": "Espressif Inc.", "os_type": 0, "type": "0", "nickName": "FakeNickName", "defaultType": "0", "from": "nmpClient", "ROG": "0", "amesh_isRe": "0", "amesh_bind_mac": "", "amesh_bind_band": "0" }, "00:00:00:00:00:43": { "mac": "00:00:00:00:00:43", "name": "FakeName", "vendor": "Microsoft", "vendorclass": "MSFT 5.0", "os_type": 2, "type": "30", "nickName": "", "defaultType": "30", "from": "nmpClient", "ROG": "0", "amesh_isRe": "0", "amesh_bind_mac": "", "amesh_bind_band": "0" }, "00:00:00:00:00:45": { "mac": "00:00:00:00:00:45", "name": "FakeName", "vendor": "", "vendorclass": "android-dhcp-11", "os_type": 1, "type": "31", "nickName": "", "defaultType": "31", "from": "nmpClient", "ROG": "0", "amesh_isRe": "0", "amesh_bind_mac": "", "amesh_bind_band": "0" }, "00:00:00:00:00:46": { "mac": "00:00:00:00:00:46", "name": "FakeName", "vendor": "Samsung", "vendorclass": "", "os_type": 0, "type": "0", "nickName": "", "defaultType": "0", "from": "nmpClient", "ROG": "0", "amesh_isRe": "0", "amesh_bind_mac": "", "amesh_bind_band": "0" }, "00:00:00:00:00:47": { "mac": "00:00:00:00:00:47", "name": "FakeName", "vendor": "MSFT 5.0", "vendorclass": "MSFT 5.0", "os_type": 2, "type": "30", "nickName": "FakeNickName", "defaultType": "30", "from": "nmpClient", "ROG": "0", "amesh_isRe": "0", "amesh_bind_mac": "", "amesh_bind_band": "0" }, "00:00:00:00:00:48": { "mac": "00:00:00:00:00:48", "name": "FakeName", "vendor": "Samsung Group", "vendorclass": "android-dhcp-10", "os_type": 1, "type": "9", "nickName": "", "defaultType": "9", "from": "nmpClient", "ROG": "0", "amesh_isRe": "0", "amesh_bind_mac": "", "amesh_bind_band": "0" }, "00:00:00:00:00:49": { "mac": "00:00:00:00:00:49", "name": "FakeName", "vendor": "Espressif Inc.", "vendorclass": "Espressif Inc.", "os_type": 2, "type": "0", "nickName": "FakeNickName", "defaultType": "1", "from": "nmpClient", "ROG": "0", "amesh_isRe": "0", "amesh_bind_mac": "", "amesh_bind_band": "0" }, "00:00:00:00:00:50": { "mac": "00:00:00:00:00:50", "name": "FakeName", "vendor": "Asus", "vendorclass": "Espressif Inc.", "type": "34", "os_type": 0, "nickName": "FakeNickName", "defaultType": "24", "from": "nmpClient", "ROG": "0", "amesh_isRe": "0", "amesh_bind_mac": "", "amesh_bind_band": "0" }, "00:00:00:00:00:51": { "mac": "00:00:00:00:00:51", "name": "FakeName", "vendor": "android-dhcp-10", "vendorclass": "android-dhcp-10", "type": "9", "os_type": 1, "nickName": "FakeNickName", "defaultType": "9", "from": "nmpClient", "ROG": "0", "amesh_isRe": "0", "amesh_bind_mac": "", "amesh_bind_band": "0" }, "00:00:00:00:00:52": { "mac": "00:00:00:00:00:52", "name": "FakeName", "vendor": "android-dhcp-10", "vendorclass": "android-dhcp-10", "type": "9", "os_type": 1, "nickName": "", "defaultType": "9", "from": "nmpClient", "ROG": "0", "amesh_isRe": "0", "amesh_bind_mac": "", "amesh_bind_band": "0" }, "00:00:00:00:00:53": { "mac": "00:00:00:00:00:53", "name": "FakeName", "vendor": "Raspberry Pi Foundation", "vendorclass": "Raspberry Pi Foundation", "type": "0", "os_type": 0, "nickName": "", "defaultType": "0", "from": "nmpClient", "ROG": "0", "amesh_isRe": "0", "amesh_bind_mac": "", "amesh_bind_band": "0" }, "00:00:00:00:00:54": { "mac": "00:00:00:00:00:54", "name": "FakeName", "vendor": "Raspberry Pi Foundation", "vendorclass": "Raspberry Pi Foundation", "type": "0", "os_type": 0, "nickName": "FakeNickName", "defaultType": "0", "from": "nmpClient", "ROG": "0", "amesh_isRe": "0", "amesh_bind_mac": "", "amesh_bind_band": "0" }, "00:00:00:00:00:15": { "mac": "00:00:00:00:00:15", "name": "FakeName", "vendor": "Synology Incorporated", "vendorclass": "Synology", "type": "5", "os_type": 0, "nickName": "FakeNickName", "defaultType": "4", "from": "nmpClient", "ROG": "0", "amesh_isRe": "0", "amesh_bind_mac": "", "amesh_bind_band": "0" }, "00:00:00:00:00:55": { "mac": "00:00:00:00:00:55", "name": "FakeName", "vendor": "Synology Incorporated", "vendorclass": "Synology", "type": "4", "os_type": 0, "nickName": "FakeNickName", "defaultType": "4", "from": "nmpClient", "ROG": "0", "amesh_isRe": "0", "amesh_bind_mac": "", "amesh_bind_band": "0" }, "00:00:00:00:00:56": { "mac": "00:00:00:00:00:56", "name": "", "vendor": "Microsoft Corp.", "vendorclass": "", "type": "34", "os_type": 0, "nickName": "", "defaultType": "34", "from": "nmpClient", "ROG": "0", "amesh_isRe": "0", "amesh_bind_mac": "", "amesh_bind_band": "0" }, "00:00:00:00:00:57": { "mac": "00:00:00:00:00:57", "name": "MSFT 5.0", "vendor": "MSFT 5.0", "vendorclass": "MSFT 5.0", "type": "30", "os_type": 2, "sdn_idx": 0, "online": "0", "wireless": 2, "is_wireless": 2, "conn_ts": 1691951484, "nickName": "", "defaultType": "30", "from": "nmpClient", "ROG": "0", "amesh_isRe": "0", "amesh_bind_mac": "", "amesh_bind_band": "0" }, "00:00:00:00:00:58": { "mac": "00:00:00:00:00:58", "name": "FakeName", "vendor": "MSFT 5.0 XBOX", "vendorclass": "MSFT 5.0 XBOX", "type": "34", "os_type": 2, "sdn_idx": 0, "online": "0", "wireless": 0, "is_wireless": 0, "conn_ts": 0, "nickName": "FakeNickName", "defaultType": "7", "from": "nmpClient", "ROG": "0", "amesh_isRe": "0", "amesh_bind_mac": "", "amesh_bind_band": "0" }, "00:00:00:00:00:07": { "mac": "00:00:00:00:00:07", "name": "FakeName", "vendor": "", "vendorclass": "", "type": "0", "os_type": 0, "sdn_idx": 0, "online": "1", "wireless": 1, "is_wireless": 1, "conn_ts": 1700405333, "nickName": "FakeNickName", "defaultType": "0", "from": "nmpClient", "ROG": "0", "amesh_isRe": "0", "amesh_bind_mac": "", "amesh_bind_band": "0" }, "00:00:00:00:00:59": { "mac": "00:00:00:00:00:59", "name": "", "vendor": "Apple, Inc.", "vendorclass": "", "type": "0", "os_type": 5, "sdn_idx": 0, "online": "0", "wireless": 2, "is_wireless": 2, "conn_ts": 1698701168, "nickName": "00:00:00:00:00:59", "defaultType": "10", "from": "nmpClient", "ROG": "0", "amesh_isRe": "0", "amesh_bind_mac": "", "amesh_bind_band": "0" }, "00:00:00:00:00:16": { "mac": "00:00:00:00:00:16", "name": "FakeName", "vendor": "", "vendorclass": "", "type": "10", "os_type": 0, "sdn_idx": 0, "online": "1", "wireless": 2, "is_wireless": 2, "conn_ts": 1700405333, "nickName": "FakeNickName", "defaultType": "0", "from": "nmpClient", "ROG": "0", "amesh_isRe": "0", "amesh_bind_mac": "", "amesh_bind_band": "0" }, "00:00:00:00:00:60": { "mac": "00:00:00:00:00:60", "name": "", "vendor": "", "vendorclass": "", "type": "0", "os_type": 0, "sdn_idx": 0, "online": "0", "wireless": 2, "is_wireless": 2, "conn_ts": 1699113854, "nickName": "", "defaultType": "0", "from": "nmpClient", "ROG": "0", "amesh_isRe": "0", "amesh_bind_mac": "", "amesh_bind_band": "0" }, "maclist": [ "00:00:00:00:00:10", "00:00:00:00:00:17", "00:00:00:00:00:11", "00:00:00:00:00:21", "00:00:00:00:00:05", "00:00:00:00:00:03", "00:00:00:00:00:22", "00:00:00:00:00:20", "00:00:00:00:00:23", "00:00:00:00:00:19", "00:00:00:00:00:24", "00:00:00:00:00:25", "00:00:00:00:00:12", "00:00:00:00:00:26", "00:00:00:00:00:06", "00:00:00:00:00:08", "00:00:00:00:00:13", "00:00:00:00:00:27", "00:00:00:00:00:14", "00:00:00:00:00:28", "00:00:00:00:00:04", "00:00:00:00:00:29", "00:00:00:00:00:30", "00:00:00:00:00:18", "00:00:00:00:00:31", "00:00:00:00:00:32", "00:00:00:00:00:02", "00:00:00:00:00:33", "00:00:00:00:00:34", "00:00:00:00:00:35", "00:00:00:00:00:44", "00:00:00:00:00:36", "00:00:00:00:00:37", "00:00:00:00:00:38", "00:00:00:00:00:39", "00:00:00:00:00:40", "00:00:00:00:00:41", "00:00:00:00:00:01", "00:00:00:00:00:09", "00:00:00:00:00:42", "00:00:00:00:00:43", "00:00:00:00:00:45", "00:00:00:00:00:46", "00:00:00:00:00:47", "00:00:00:00:00:48", "00:00:00:00:00:49", "00:00:00:00:00:50", "00:00:00:00:00:51", "00:00:00:00:00:52", "00:00:00:00:00:53", "00:00:00:00:00:54", "00:00:00:00:00:15", "00:00:00:00:00:55", "00:00:00:00:00:56", "00:00:00:00:00:57", "00:00:00:00:00:58", "00:00:00:00:00:07", "00:00:00:00:00:59", "00:00:00:00:00:16", "00:00:00:00:00:60" ], "ClientAPILevel": "5" }] +} +networkmap_fullscan = '2'; +if(networkmap_fullscan == 2) genClientList(); + diff --git a/tests/test_data/rt_ax88u_merlin_388/update_clients_001.py b/tests/test_data/rt_ax88u_merlin_388/update_clients_001.py new file mode 100644 index 0000000..ef6004b --- /dev/null +++ b/tests/test_data/rt_ax88u_merlin_388/update_clients_001.py @@ -0,0 +1,1560 @@ +"""Result of processing update_clients_001.content.""" + +from asusrouter import AsusData + +expected_result = { + AsusData.CLIENTS: { + "00:00:00:00:00:01": { + "type": "0", + "defaultType": "0", + "name": "Espressif Inc ", + "nickName": "FakeNickName", + "ip": "192.168.1.147", + "mac": "00:00:00:00:00:01", + "macRepeat": "0", + "isGateway": "0", + "isWebServer": "0", + "isPrinter": "0", + "isITunes": "0", + "dpiType": None, + "dpiDevice": None, + "vendor": "Espressif Inc.", + "isWL": "1", + "isGN": None, + "isOnline": "1", + "ssid": None, + "isLogin": "0", + "opMode": "0", + "rssi": "-43", + "curTx": "54", + "curRx": "11", + "totalTx": None, + "totalRx": None, + "wlConnectTime": "141:25:19", + "ipMethod": "Manual", + "ROG": "0", + "group": None, + "callback": None, + "keeparp": None, + "qosLevel": None, + "wtfast": "0", + "internetMode": "allow", + "internetState": 1, + "amesh_isReClient": "1", + "amesh_papMac": "00:00:00:00:00:00", + "amesh_bind_mac": None, + "amesh_bind_band": "0", + "vendorclass": "Espressif Inc.", + "os_type": 0, + "amesh_isRe": "0", + }, + "00:00:00:00:00:02": { + "type": "30", + "defaultType": "30", + "name": "MSFT 5 0", + "nickName": "FakeNickName", + "ip": "192.168.1.22", + "mac": "00:00:00:00:00:02", + "macRepeat": "0", + "isGateway": "0", + "isWebServer": "0", + "isPrinter": "0", + "isITunes": "0", + "dpiType": None, + "dpiDevice": None, + "vendor": "MSFT 5.0", + "isWL": "2", + "isGN": None, + "isOnline": "0", + "ssid": None, + "isLogin": "0", + "opMode": "0", + "rssi": "-25", + "curTx": "2401.9", + "curRx": "1225", + "totalTx": None, + "totalRx": None, + "wlConnectTime": "00:00:33", + "ipMethod": "Manual", + "ROG": "0", + "group": None, + "callback": None, + "keeparp": None, + "qosLevel": None, + "wtfast": "0", + "internetMode": "allow", + "internetState": "1", + "amesh_bind_mac": None, + "amesh_bind_band": "0", + "vendorclass": "MSFT 5.0", + "os_type": 2, + "amesh_isRe": "0", + }, + "00:00:00:00:00:03": { + "type": "30", + "defaultType": "30", + "name": "FakeName", + "nickName": "FakeNickName", + "ip": "192.168.1.10", + "mac": "00:00:00:00:00:03", + "macRepeat": "0", + "isGateway": "0", + "isWebServer": "0", + "isPrinter": "0", + "isITunes": "0", + "dpiType": None, + "dpiDevice": None, + "vendor": "MSFT 5.0", + "isWL": "0", + "isGN": None, + "isOnline": "1", + "ssid": None, + "isLogin": "0", + "opMode": "0", + "rssi": "0", + "curTx": None, + "curRx": None, + "totalTx": None, + "totalRx": None, + "wlConnectTime": None, + "ipMethod": "Manual", + "ROG": "0", + "group": None, + "callback": None, + "keeparp": None, + "qosLevel": None, + "wtfast": "0", + "internetMode": "allow", + "internetState": "1", + "amesh_isReClient": "1", + "amesh_papMac": "00:00:00:00:00:00", + "amesh_bind_mac": None, + "amesh_bind_band": "0", + "vendorclass": "MSFT 5.0", + "os_type": 2, + "amesh_isRe": "0", + }, + "00:00:00:00:00:04": { + "type": "0", + "defaultType": "0", + "name": "FakeName", + "nickName": "FakeNickName", + "ip": "192.168.1.112", + "mac": "00:00:00:00:00:04", + "macRepeat": "0", + "isGateway": "0", + "isWebServer": "0", + "isPrinter": "0", + "isITunes": "0", + "dpiType": None, + "dpiDevice": None, + "vendor": "Espressif Inc.", + "isWL": "1", + "isGN": None, + "isOnline": "1", + "ssid": None, + "isLogin": "0", + "opMode": "0", + "rssi": "-50", + "curTx": "57.8", + "curRx": "6", + "totalTx": None, + "totalRx": None, + "wlConnectTime": "32:24:36", + "ipMethod": "Manual", + "ROG": "0", + "group": None, + "callback": None, + "keeparp": None, + "qosLevel": None, + "wtfast": "0", + "internetMode": "block", + "internetState": 0, + "amesh_isReClient": "1", + "amesh_papMac": "00:00:00:00:00:00", + "amesh_bind_mac": None, + "amesh_bind_band": "0", + "vendorclass": "Espressif Inc.", + "os_type": 0, + "amesh_isRe": "0", + }, + "00:00:00:00:00:05": { + "type": "0", + "defaultType": "0", + "name": "FakeName", + "nickName": "FakeNickName", + "ip": "192.168.1.12", + "mac": "00:00:00:00:00:05", + "macRepeat": "0", + "isGateway": "0", + "isWebServer": "0", + "isPrinter": "0", + "isITunes": "0", + "dpiType": None, + "dpiDevice": None, + "vendor": "Microsoft Corporation", + "isWL": "0", + "isGN": None, + "isOnline": "1", + "ssid": None, + "isLogin": "0", + "opMode": "0", + "rssi": "0", + "curTx": None, + "curRx": None, + "totalTx": None, + "totalRx": None, + "wlConnectTime": None, + "ipMethod": "Manual", + "ROG": "0", + "group": None, + "callback": None, + "keeparp": None, + "qosLevel": None, + "wtfast": "0", + "internetMode": "allow", + "internetState": "1", + "amesh_isReClient": "1", + "amesh_papMac": "00:00:00:00:00:00", + "amesh_bind_mac": None, + "amesh_bind_band": "0", + "vendorclass": "Microsoft", + "os_type": 2, + "amesh_isRe": "0", + }, + "00:00:00:00:00:06": { + "type": "6", + "defaultType": "6", + "name": "FakeName", + "nickName": "FakeNickName", + "ip": "192.168.1.102", + "mac": "00:00:00:00:00:06", + "macRepeat": "0", + "isGateway": "0", + "isWebServer": "0", + "isPrinter": "0", + "isITunes": "0", + "dpiType": None, + "dpiDevice": None, + "vendor": "AzureWave Technology Inc.", + "isWL": "1", + "isGN": None, + "isOnline": "1", + "ssid": None, + "isLogin": "0", + "opMode": "0", + "rssi": "-44", + "curTx": "58.5", + "curRx": "72.2", + "totalTx": None, + "totalRx": None, + "wlConnectTime": "141:25:19", + "ipMethod": "Manual", + "ROG": "0", + "group": None, + "callback": None, + "keeparp": None, + "qosLevel": None, + "wtfast": "0", + "internetMode": "allow", + "internetState": "1", + "amesh_isReClient": "1", + "amesh_papMac": "00:00:00:00:00:00", + "amesh_bind_mac": None, + "amesh_bind_band": "0", + "vendorclass": "AzureWave Technology Inc.", + "os_type": 5, + "amesh_isRe": "0", + }, + "00:00:00:00:00:07": { + "type": "0", + "defaultType": "0", + "name": "FakeName", + "nickName": "FakeNickName", + "ip": "192.168.1.73", + "mac": "00:00:00:00:00:07", + "macRepeat": "0", + "isGateway": "0", + "isWebServer": "0", + "isPrinter": "0", + "isITunes": "0", + "dpiType": None, + "dpiDevice": None, + "vendor": None, + "isWL": "1", + "isGN": None, + "isOnline": "1", + "ssid": None, + "isLogin": "0", + "opMode": "0", + "rssi": "-34", + "curTx": "72.2", + "curRx": "6", + "totalTx": None, + "totalRx": None, + "wlConnectTime": "08:25:35", + "ipMethod": "Manual", + "ROG": "0", + "group": None, + "callback": None, + "keeparp": None, + "qosLevel": None, + "wtfast": "0", + "internetMode": "allow", + "internetState": "1", + "amesh_isReClient": "1", + "amesh_papMac": "00:00:00:00:00:00", + "amesh_bind_mac": None, + "amesh_bind_band": "0", + "vendorclass": None, + "os_type": 0, + "sdn_idx": 0, + "online": "1", + "wireless": 1, + "is_wireless": 1, + "conn_ts": 1700405333, + "amesh_isRe": "0", + }, + "00:00:00:00:00:08": { + "type": "0", + "defaultType": "0", + "name": "Espressif Inc ", + "nickName": "FakeNickName", + "ip": "192.168.1.116", + "mac": "00:00:00:00:00:08", + "macRepeat": "0", + "isGateway": "0", + "isWebServer": "0", + "isPrinter": "0", + "isITunes": "0", + "dpiType": None, + "dpiDevice": None, + "vendor": "Espressif Inc.", + "isWL": "1", + "isGN": None, + "isOnline": "1", + "ssid": None, + "isLogin": "0", + "opMode": "0", + "rssi": "-26", + "curTx": "65", + "curRx": "54", + "totalTx": None, + "totalRx": None, + "wlConnectTime": "32:24:41", + "ipMethod": "Manual", + "ROG": "0", + "group": None, + "callback": None, + "keeparp": None, + "qosLevel": None, + "wtfast": "0", + "internetMode": "block", + "internetState": 0, + "amesh_isReClient": "1", + "amesh_papMac": "00:00:00:00:00:00", + "amesh_bind_mac": None, + "amesh_bind_band": "0", + "vendorclass": "Espressif Inc.", + "os_type": 0, + "amesh_isRe": "0", + }, + "00:00:00:00:00:09": { + "type": "0", + "defaultType": "0", + "name": "FakeName", + "nickName": "FakeNickName", + "ip": "192.168.1.146", + "mac": "00:00:00:00:00:09", + "macRepeat": "0", + "isGateway": "0", + "isWebServer": "0", + "isPrinter": "0", + "isITunes": "0", + "dpiType": None, + "dpiDevice": None, + "vendor": "Espressif Inc.", + "isWL": "1", + "isGN": None, + "isOnline": "1", + "ssid": None, + "isLogin": "0", + "opMode": "0", + "rssi": "-53", + "curTx": "54", + "curRx": "11", + "totalTx": None, + "totalRx": None, + "wlConnectTime": "141:25:19", + "ipMethod": "Manual", + "ROG": "0", + "group": None, + "callback": None, + "keeparp": None, + "qosLevel": None, + "wtfast": "0", + "internetMode": "allow", + "internetState": "1", + "amesh_isReClient": "1", + "amesh_papMac": "00:00:00:00:00:00", + "amesh_bind_mac": None, + "amesh_bind_band": "0", + "vendorclass": "Espressif Inc.", + "os_type": 0, + "amesh_isRe": "0", + }, + "00:00:00:00:00:10": { + "type": "30", + "defaultType": "30", + "name": "FakeName", + "nickName": "FakeNickName", + "ip": "192.168.1.21", + "mac": "00:00:00:00:00:10", + "macRepeat": "0", + "isGateway": "0", + "isWebServer": "0", + "isPrinter": "0", + "isITunes": "0", + "dpiType": None, + "dpiDevice": None, + "vendor": "MSFT 5.0", + "isWL": "0", + "isGN": None, + "isOnline": "1", + "ssid": None, + "isLogin": "1", + "opMode": "0", + "rssi": "0", + "curTx": None, + "curRx": None, + "totalTx": None, + "totalRx": None, + "wlConnectTime": None, + "ipMethod": "Manual", + "ROG": "0", + "group": None, + "callback": None, + "keeparp": None, + "qosLevel": None, + "wtfast": "0", + "internetMode": "allow", + "internetState": "1", + "amesh_isReClient": "1", + "amesh_papMac": "00:00:00:00:00:00", + "amesh_bind_mac": None, + "amesh_bind_band": "0", + "vendorclass": "MSFT 5.0", + "os_type": 2, + "amesh_isRe": "0", + }, + "00:00:00:00:00:11": { + "type": "0", + "defaultType": "0", + "name": "FakeName", + "nickName": "FakeNickName", + "ip": "192.168.1.11", + "mac": "00:00:00:00:00:11", + "macRepeat": "0", + "isGateway": "0", + "isWebServer": "0", + "isPrinter": "0", + "isITunes": "0", + "dpiType": None, + "dpiDevice": None, + "vendor": "Microsoft Corporation", + "isWL": "0", + "isGN": None, + "isOnline": "1", + "ssid": None, + "isLogin": "0", + "opMode": "0", + "rssi": "0", + "curTx": None, + "curRx": None, + "totalTx": None, + "totalRx": None, + "wlConnectTime": None, + "ipMethod": "Manual", + "ROG": "0", + "group": None, + "callback": None, + "keeparp": None, + "qosLevel": None, + "wtfast": "0", + "internetMode": "allow", + "internetState": "1", + "amesh_isReClient": "1", + "amesh_papMac": "00:00:00:00:00:00", + "amesh_bind_mac": None, + "amesh_bind_band": "0", + "vendorclass": "Microsoft", + "os_type": 2, + "amesh_isRe": "0", + }, + "00:00:00:00:00:12": { + "type": "0", + "defaultType": "0", + "name": "Espressif Inc ", + "nickName": "FakeNickName", + "ip": "192.168.1.113", + "mac": "00:00:00:00:00:12", + "macRepeat": "0", + "isGateway": "0", + "isWebServer": "0", + "isPrinter": "0", + "isITunes": "0", + "dpiType": None, + "dpiDevice": None, + "vendor": "Espressif Inc.", + "isWL": "1", + "isGN": None, + "isOnline": "1", + "ssid": None, + "isLogin": "0", + "opMode": "0", + "rssi": "-58", + "curTx": "65", + "curRx": "54", + "totalTx": None, + "totalRx": None, + "wlConnectTime": "32:24:35", + "ipMethod": "Manual", + "ROG": "0", + "group": None, + "callback": None, + "keeparp": None, + "qosLevel": None, + "wtfast": "0", + "internetMode": "block", + "internetState": 0, + "amesh_isReClient": "1", + "amesh_papMac": "00:00:00:00:00:00", + "amesh_bind_mac": None, + "amesh_bind_band": "0", + "vendorclass": "Espressif Inc.", + "os_type": 0, + "amesh_isRe": "0", + }, + "00:00:00:00:00:13": { + "type": "0", + "defaultType": "0", + "name": "FakeName", + "nickName": "FakeNickName", + "ip": "192.168.1.101", + "mac": "00:00:00:00:00:13", + "macRepeat": "0", + "isGateway": "0", + "isWebServer": "0", + "isPrinter": "0", + "isITunes": "0", + "dpiType": None, + "dpiDevice": None, + "vendor": "Seiko Epson Corporation", + "isWL": "1", + "isGN": None, + "isOnline": "1", + "ssid": None, + "isLogin": "0", + "opMode": "0", + "rssi": "-41", + "curTx": "130", + "curRx": "1", + "totalTx": None, + "totalRx": None, + "wlConnectTime": "141:25:16", + "ipMethod": "Manual", + "ROG": "0", + "group": None, + "callback": None, + "keeparp": None, + "qosLevel": None, + "wtfast": "0", + "internetMode": "allow", + "internetState": "1", + "amesh_isReClient": "1", + "amesh_papMac": "00:00:00:00:00:00", + "amesh_bind_mac": None, + "amesh_bind_band": "0", + "vendorclass": "Seiko Epson Corporation", + "os_type": 0, + "amesh_isRe": "0", + }, + "00:00:00:00:00:14": { + "type": "0", + "defaultType": "0", + "name": "FakeName", + "nickName": "FakeNickName", + "ip": "192.168.1.115", + "mac": "00:00:00:00:00:14", + "macRepeat": "0", + "isGateway": "0", + "isWebServer": "0", + "isPrinter": "0", + "isITunes": "0", + "dpiType": None, + "dpiDevice": None, + "vendor": "Espressif Inc.", + "isWL": "1", + "isGN": None, + "isOnline": "1", + "ssid": None, + "isLogin": "0", + "opMode": "0", + "rssi": "-41", + "curTx": "54", + "curRx": "54", + "totalTx": None, + "totalRx": None, + "wlConnectTime": "116:41:53", + "ipMethod": "Manual", + "ROG": "0", + "group": None, + "callback": None, + "keeparp": None, + "qosLevel": None, + "wtfast": "0", + "internetMode": "block", + "internetState": 0, + "amesh_isReClient": "1", + "amesh_papMac": "00:00:00:00:00:00", + "amesh_bind_mac": None, + "amesh_bind_band": "0", + "vendorclass": "Espressif Inc.", + "os_type": 0, + "amesh_isRe": "0", + }, + "00:00:00:00:00:15": { + "type": "5", + "defaultType": "4", + "name": "FakeName", + "nickName": "FakeNickName", + "ip": "192.168.1.15", + "mac": "00:00:00:00:00:15", + "macRepeat": "0", + "isGateway": "0", + "isWebServer": "0", + "isPrinter": "0", + "isITunes": "0", + "dpiType": None, + "dpiDevice": None, + "vendor": "Synology Incorporated", + "isWL": "0", + "isGN": None, + "isOnline": "1", + "ssid": None, + "isLogin": "0", + "opMode": "0", + "rssi": "0", + "curTx": None, + "curRx": None, + "totalTx": None, + "totalRx": None, + "wlConnectTime": None, + "ipMethod": "Manual", + "ROG": "0", + "group": None, + "callback": None, + "keeparp": None, + "qosLevel": None, + "wtfast": "0", + "internetMode": "allow", + "internetState": "1", + "amesh_isReClient": "1", + "amesh_papMac": "00:00:00:00:00:00", + "amesh_bind_mac": None, + "amesh_bind_band": "0", + "vendorclass": "Synology", + "os_type": 0, + "amesh_isRe": "0", + }, + "00:00:00:00:00:16": { + "type": "10", + "defaultType": "0", + "name": "FakeName", + "nickName": "FakeNickName", + "ip": "192.168.1.25", + "mac": "00:00:00:00:00:16", + "macRepeat": "0", + "isGateway": "0", + "isWebServer": "0", + "isPrinter": "0", + "isITunes": "0", + "dpiType": None, + "dpiDevice": None, + "vendor": None, + "isWL": "2", + "isGN": None, + "isOnline": "1", + "ssid": None, + "isLogin": "0", + "opMode": "0", + "rssi": "-40", + "curTx": "2041.7", + "curRx": "24", + "totalTx": None, + "totalRx": None, + "wlConnectTime": "13:39:08", + "ipMethod": "DHCP", + "ROG": "0", + "group": None, + "callback": None, + "keeparp": None, + "qosLevel": None, + "wtfast": "0", + "internetMode": "allow", + "internetState": "1", + "amesh_isReClient": "1", + "amesh_papMac": "00:00:00:00:00:00", + "amesh_bind_mac": None, + "amesh_bind_band": "0", + "vendorclass": None, + "os_type": 0, + "sdn_idx": 0, + "online": "1", + "wireless": 2, + "is_wireless": 2, + "conn_ts": 1700405333, + "amesh_isRe": "0", + }, + "00:00:00:00:00:17": { + "type": "0", + "defaultType": "0", + "name": "FakeName", + "nickName": "FakeNickName", + "ip": "192.168.1.114", + "mac": "00:00:00:00:00:17", + "macRepeat": "0", + "isGateway": "0", + "isWebServer": "0", + "isPrinter": "0", + "isITunes": "0", + "dpiType": None, + "dpiDevice": None, + "vendor": "Espressif Inc.", + "isWL": "1", + "isGN": None, + "isOnline": "1", + "ssid": None, + "isLogin": "0", + "opMode": "0", + "rssi": "-52", + "curTx": "54", + "curRx": "6", + "totalTx": None, + "totalRx": None, + "wlConnectTime": "111:46:30", + "ipMethod": "Manual", + "ROG": "0", + "group": None, + "callback": None, + "keeparp": None, + "qosLevel": None, + "wtfast": "0", + "internetMode": "block", + "internetState": 0, + "amesh_isReClient": "1", + "amesh_papMac": "00:00:00:00:00:00", + "amesh_bind_mac": None, + "amesh_bind_band": "0", + "vendorclass": "Espressif Inc.", + "os_type": 0, + "amesh_isRe": "0", + }, + "00:00:00:00:00:18": { + "type": "0", + "defaultType": "0", + "name": "FakeName", + "nickName": "FakeNickName", + "ip": "192.168.1.52", + "mac": "00:00:00:00:00:18", + "macRepeat": "0", + "isGateway": "0", + "isWebServer": "0", + "isPrinter": "0", + "isITunes": "0", + "dpiType": None, + "dpiDevice": None, + "vendor": "Apple, Inc.", + "isWL": "2", + "isGN": None, + "isOnline": "1", + "ssid": None, + "isLogin": "0", + "opMode": "0", + "rssi": "-43", + "curTx": "1134.2", + "curRx": "24", + "totalTx": None, + "totalRx": None, + "wlConnectTime": "05:18:53", + "ipMethod": "Manual", + "ROG": "0", + "group": None, + "callback": None, + "keeparp": None, + "qosLevel": None, + "wtfast": "0", + "internetMode": "allow", + "internetState": "1", + "amesh_isReClient": "1", + "amesh_papMac": "00:00:00:00:00:00", + "amesh_bind_mac": None, + "amesh_bind_band": "0", + "vendorclass": "Apple", + "os_type": 5, + "amesh_isRe": "0", + }, + "00:00:00:00:00:19": { + "type": "31", + "defaultType": "9", + "name": "FakeName", + "nickName": "FakeNickName", + "ip": "192.168.1.24", + "mac": "00:00:00:00:00:19", + "macRepeat": "0", + "isGateway": "0", + "isWebServer": "0", + "isPrinter": "0", + "isITunes": "0", + "dpiType": None, + "dpiDevice": None, + "vendor": "android-dhcp-13", + "isWL": "2", + "isGN": None, + "isOnline": "1", + "ssid": None, + "isLogin": "0", + "opMode": "0", + "rssi": "-27", + "curTx": "1134.2", + "curRx": "6", + "totalTx": None, + "totalRx": None, + "wlConnectTime": "08:27:14", + "ipMethod": "Manual", + "ROG": "0", + "group": None, + "callback": None, + "keeparp": None, + "qosLevel": None, + "wtfast": "0", + "internetMode": "allow", + "internetState": "1", + "amesh_isReClient": "1", + "amesh_papMac": "00:00:00:00:00:00", + "amesh_bind_mac": None, + "amesh_bind_band": "0", + "vendorclass": "android-dhcp-13", + "os_type": 1, + "amesh_isRe": "0", + }, + "00:00:00:00:00:20": { + "type": "0", + "defaultType": "0", + "name": "FakeName", + "nickName": "FakeNickName", + "ip": "192.168.1.111", + "mac": "00:00:00:00:00:20", + "macRepeat": "0", + "isGateway": "0", + "isWebServer": "0", + "isPrinter": "0", + "isITunes": "0", + "dpiType": None, + "dpiDevice": None, + "vendor": "Espressif Inc.", + "isWL": "1", + "isGN": None, + "isOnline": "1", + "ssid": None, + "isLogin": "0", + "opMode": "0", + "rssi": "-47", + "curTx": "72.2", + "curRx": "6", + "totalTx": None, + "totalRx": None, + "wlConnectTime": "32:01:57", + "ipMethod": "Manual", + "ROG": "0", + "group": None, + "callback": None, + "keeparp": None, + "qosLevel": None, + "wtfast": "0", + "internetMode": "block", + "internetState": 0, + "amesh_isReClient": "1", + "amesh_papMac": "00:00:00:00:00:00", + "amesh_bind_mac": None, + "amesh_bind_band": "0", + "vendorclass": "Espressif Inc.", + "os_type": 0, + "amesh_isRe": "0", + }, + "00:00:00:00:00:29": { + "type": "31", + "defaultType": "9", + "name": "FakeName", + "nickName": "FakeNickName", + "ip": "192.168.1.51", + "mac": "00:00:00:00:00:29", + "macRepeat": "0", + "isGateway": "0", + "isWebServer": "0", + "isPrinter": "0", + "isITunes": "0", + "dpiType": None, + "dpiDevice": None, + "vendor": "android-dhcp-10", + "isWL": "2", + "isGN": None, + "isOnline": "0", + "ssid": None, + "isLogin": "0", + "opMode": "0", + "rssi": "-46", + "curTx": "780", + "curRx": "975", + "totalTx": None, + "totalRx": None, + "wlConnectTime": "24:22:53", + "ipMethod": "Manual", + "ROG": "0", + "group": None, + "callback": None, + "keeparp": None, + "qosLevel": None, + "wtfast": "0", + "internetMode": "allow", + "internetState": "1", + "amesh_bind_mac": None, + "amesh_bind_band": "0", + "vendorclass": "android-dhcp-10", + "os_type": 1, + "amesh_isRe": "0", + }, + "00:00:00:00:00:58": { + "type": "34", + "defaultType": "7", + "name": "FakeName", + "nickName": "FakeNickName", + "ip": "192.168.1.103", + "mac": "00:00:00:00:00:58", + "macRepeat": "0", + "isGateway": "0", + "isWebServer": "0", + "isPrinter": "0", + "isITunes": "0", + "dpiType": None, + "dpiDevice": None, + "vendor": "MSFT 5.0 XBOX", + "isWL": "0", + "isGN": None, + "isOnline": "0", + "ssid": None, + "isLogin": "0", + "opMode": "0", + "rssi": "0", + "curTx": None, + "curRx": None, + "totalTx": None, + "totalRx": None, + "wlConnectTime": None, + "ipMethod": "Manual", + "ROG": "0", + "group": None, + "callback": None, + "keeparp": None, + "qosLevel": None, + "wtfast": "0", + "internetMode": "allow", + "internetState": "1", + "amesh_bind_mac": None, + "amesh_bind_band": "0", + "vendorclass": "MSFT 5.0 XBOX", + "os_type": 2, + "sdn_idx": 0, + "online": "0", + "wireless": 0, + "is_wireless": 0, + "conn_ts": 0, + "amesh_isRe": "0", + }, + "00:00:00:00:00:33": { + "type": "0", + "defaultType": "0", + "name": "FakeName", + "nickName": None, + "ip": "192.168.1.131", + "mac": "00:00:00:00:00:33", + "macRepeat": "0", + "isGateway": "0", + "isWebServer": "0", + "isPrinter": "0", + "isITunes": "0", + "dpiType": None, + "dpiDevice": None, + "vendor": "Samsung Electronics Co., Ltd.", + "isWL": "1", + "isGN": None, + "isOnline": "0", + "ssid": None, + "isLogin": "0", + "opMode": "0", + "rssi": "-49", + "curTx": "65", + "curRx": "65", + "totalTx": None, + "totalRx": None, + "wlConnectTime": "00:00:42", + "ipMethod": "DHCP", + "ROG": "0", + "group": None, + "callback": None, + "keeparp": None, + "qosLevel": None, + "wtfast": "0", + "internetMode": "allow", + "internetState": "1", + "amesh_bind_mac": None, + "amesh_bind_band": "0", + "vendorclass": "Samsung", + "os_type": 0, + "amesh_isRe": "0", + }, + "00:00:00:00:00:21": { + "mac": "00:00:00:00:00:21", + "name": "FakeName", + "vendor": "TP-LINK", + "vendorclass": None, + "os_type": 0, + "type": "0", + "nickName": "FakeNickName", + "defaultType": "0", + "ROG": "0", + "amesh_isRe": "0", + "amesh_bind_mac": None, + "amesh_bind_band": "0", + }, + "00:00:00:00:00:22": { + "mac": "00:00:00:00:00:22", + "name": None, + "vendor": None, + "vendorclass": None, + "os_type": 0, + "type": "0", + "nickName": None, + "defaultType": "0", + "ROG": "0", + "amesh_isRe": "0", + "amesh_bind_mac": None, + "amesh_bind_band": "0", + }, + "00:00:00:00:00:23": { + "mac": "00:00:00:00:00:23", + "name": "FakeName", + "vendor": "Asus", + "vendorclass": "udhcp 1.25.1", + "os_type": 0, + "type": "24", + "nickName": "FakeNickName", + "defaultType": "2", + "ROG": "0", + "amesh_isRe": "0", + "amesh_bind_mac": None, + "amesh_bind_band": "0", + }, + "00:00:00:00:00:24": { + "mac": "00:00:00:00:00:24", + "name": "FakeName", + "vendor": "Amazon", + "vendorclass": "dhcpcd-6.8.2:Linux-4.9.77+:armv", + "os_type": 3, + "type": "22", + "nickName": "FakeNickName", + "defaultType": "22", + "ROG": "0", + "amesh_isRe": "0", + "amesh_bind_mac": None, + "amesh_bind_band": "0", + }, + "00:00:00:00:00:25": { + "mac": "00:00:00:00:00:25", + "name": "FakeName", + "vendor": "Hangzhou BroadLink Technology Co., Ltd.", + "vendorclass": "Hangzhou BroadLink Technology C", + "os_type": 0, + "type": "0", + "nickName": "FakeNickName", + "defaultType": "0", + "ROG": "0", + "amesh_isRe": "0", + "amesh_bind_mac": None, + "amesh_bind_band": "0", + }, + "00:00:00:00:00:26": { + "mac": "00:00:00:00:00:26", + "name": "Espressif Inc.", + "vendor": "Espressif Inc.", + "vendorclass": None, + "os_type": 0, + "type": "0", + "nickName": "FakeNickName", + "defaultType": "0", + "ROG": "0", + "amesh_isRe": "0", + "amesh_bind_mac": None, + "amesh_bind_band": "0", + }, + "00:00:00:00:00:27": { + "mac": "00:00:00:00:00:27", + "name": "FakeName", + "vendor": "Espressif Inc.", + "vendorclass": None, + "os_type": 0, + "type": "0", + "nickName": "FakeNickName", + "defaultType": "0", + "ROG": "0", + "amesh_isRe": "0", + "amesh_bind_mac": None, + "amesh_bind_band": "0", + }, + "00:00:00:00:00:28": { + "mac": "00:00:00:00:00:28", + "name": "FakeName", + "vendor": "Intel Corporate", + "vendorclass": "MSFT 5.0", + "os_type": 2, + "type": "30", + "nickName": "FakeNickName", + "defaultType": "30", + "ROG": "0", + "amesh_isRe": "0", + "amesh_bind_mac": None, + "amesh_bind_band": "0", + }, + "00:00:00:00:00:30": { + "mac": "00:00:00:00:00:30", + "name": "FakeName", + "vendor": "android-dhcp-10", + "vendorclass": "android-dhcp-10", + "os_type": 1, + "type": "31", + "nickName": "FakeNickName", + "defaultType": "9", + "ROG": "0", + "amesh_isRe": "0", + "amesh_bind_mac": None, + "amesh_bind_band": "0", + }, + "00:00:00:00:00:31": { + "mac": "00:00:00:00:00:31", + "name": "FakeName", + "vendor": None, + "type": "30", + "vendorclass": "MSFT 5.0", + "os_type": 2, + "nickName": "00:00:00:00:00:31", + "defaultType": "30", + "ROG": "0", + "amesh_isRe": "0", + "amesh_bind_mac": None, + "amesh_bind_band": "0", + }, + "00:00:00:00:00:32": { + "mac": "00:00:00:00:00:32", + "name": "Espressif Inc.", + "vendor": "Espressif Inc.", + "vendorclass": None, + "os_type": 0, + "type": "0", + "nickName": "FakeNickName", + "defaultType": "0", + "ROG": "0", + "amesh_isRe": "0", + "amesh_bind_mac": None, + "amesh_bind_band": "0", + }, + "00:00:00:00:00:34": { + "mac": "00:00:00:00:00:34", + "name": "FakeName", + "vendor": "Samsung Electronics Co., Ltd.", + "vendorclass": "Samsung", + "os_type": 0, + "type": "0", + "nickName": "FakeNickName", + "defaultType": "0", + "ROG": "0", + "amesh_isRe": "0", + "amesh_bind_mac": None, + "amesh_bind_band": "0", + }, + "00:00:00:00:00:35": { + "mac": "00:00:00:00:00:35", + "name": "Xiaomi Communications Co Ltd", + "vendor": "Xiaomi Communications Co Ltd", + "vendorclass": "android-dhcp-9", + "os_type": 1, + "type": "31", + "nickName": None, + "defaultType": "31", + "ROG": "0", + "amesh_isRe": "0", + "amesh_bind_mac": None, + "amesh_bind_band": "0", + }, + "00:00:00:00:00:44": { + "mac": "00:00:00:00:00:44", + "name": "dhcpcd-5.5.6", + "vendor": "dhcpcd-5.5.6", + "vendorclass": "dhcpcd-5.5.6", + "os_type": 3, + "type": "0", + "nickName": "FakeNickName", + "defaultType": "22", + "ROG": "0", + "amesh_isRe": "0", + "amesh_bind_mac": None, + "amesh_bind_band": "0", + }, + "00:00:00:00:00:36": { + "mac": "00:00:00:00:00:36", + "name": "FakeName", + "vendor": None, + "type": "0", + "vendorclass": "android-dhcp-10", + "os_type": 1, + "nickName": "FakeNickName", + "defaultType": "31", + "ROG": "0", + "amesh_isRe": "0", + "amesh_bind_mac": None, + "amesh_bind_band": "0", + }, + "00:00:00:00:00:37": { + "mac": "00:00:00:00:00:37", + "name": "FakeName", + "vendor": None, + "vendorclass": "android-dhcp-12", + "os_type": 1, + "type": "31", + "nickName": None, + "defaultType": "31", + "ROG": "0", + "amesh_isRe": "0", + "amesh_bind_mac": None, + "amesh_bind_band": "0", + }, + "00:00:00:00:00:38": { + "mac": "00:00:00:00:00:38", + "name": None, + "vendor": None, + "vendorclass": None, + "os_type": 0, + "type": "0", + "nickName": None, + "defaultType": "0", + "ROG": "0", + "amesh_isRe": "0", + "amesh_bind_mac": None, + "amesh_bind_band": "0", + }, + "00:00:00:00:00:39": { + "mac": "00:00:00:00:00:39", + "name": "Intel Corporate", + "vendor": "Intel Corporate", + "vendorclass": "MSFT 5.0", + "os_type": 2, + "type": "30", + "nickName": "FakeNickName", + "defaultType": "30", + "ROG": "0", + "amesh_isRe": "0", + "amesh_bind_mac": None, + "amesh_bind_band": "0", + }, + "00:00:00:00:00:40": { + "mac": "00:00:00:00:00:40", + "name": "FakeName", + "vendor": "dhcpcd-6.6.6:Linux-3.0.35+:armv", + "vendorclass": "dhcpcd-6.6.6:Linux-3.0.35+:armv", + "os_type": 3, + "type": "22", + "nickName": None, + "defaultType": "22", + "ROG": "0", + "amesh_isRe": "0", + "amesh_bind_mac": None, + "amesh_bind_band": "0", + }, + "00:00:00:00:00:41": { + "mac": "00:00:00:00:00:41", + "name": "MSFT 5.0", + "vendor": "MSFT 5.0", + "vendorclass": "MSFT 5.0", + "os_type": 2, + "type": "30", + "nickName": "FakeNickName", + "defaultType": "30", + "ROG": "0", + "amesh_isRe": "0", + "amesh_bind_mac": None, + "amesh_bind_band": "0", + }, + "00:00:00:00:00:42": { + "mac": "00:00:00:00:00:42", + "name": "Espressif Inc.", + "vendor": "Espressif Inc.", + "vendorclass": "Espressif Inc.", + "os_type": 0, + "type": "0", + "nickName": "FakeNickName", + "defaultType": "0", + "ROG": "0", + "amesh_isRe": "0", + "amesh_bind_mac": None, + "amesh_bind_band": "0", + }, + "00:00:00:00:00:43": { + "mac": "00:00:00:00:00:43", + "name": "FakeName", + "vendor": "Microsoft", + "vendorclass": "MSFT 5.0", + "os_type": 2, + "type": "30", + "nickName": None, + "defaultType": "30", + "ROG": "0", + "amesh_isRe": "0", + "amesh_bind_mac": None, + "amesh_bind_band": "0", + }, + "00:00:00:00:00:45": { + "mac": "00:00:00:00:00:45", + "name": "FakeName", + "vendor": None, + "vendorclass": "android-dhcp-11", + "os_type": 1, + "type": "31", + "nickName": None, + "defaultType": "31", + "ROG": "0", + "amesh_isRe": "0", + "amesh_bind_mac": None, + "amesh_bind_band": "0", + }, + "00:00:00:00:00:46": { + "mac": "00:00:00:00:00:46", + "name": "FakeName", + "vendor": "Samsung", + "vendorclass": None, + "os_type": 0, + "type": "0", + "nickName": None, + "defaultType": "0", + "ROG": "0", + "amesh_isRe": "0", + "amesh_bind_mac": None, + "amesh_bind_band": "0", + }, + "00:00:00:00:00:47": { + "mac": "00:00:00:00:00:47", + "name": "FakeName", + "vendor": "MSFT 5.0", + "vendorclass": "MSFT 5.0", + "os_type": 2, + "type": "30", + "nickName": "FakeNickName", + "defaultType": "30", + "ROG": "0", + "amesh_isRe": "0", + "amesh_bind_mac": None, + "amesh_bind_band": "0", + }, + "00:00:00:00:00:48": { + "mac": "00:00:00:00:00:48", + "name": "FakeName", + "vendor": "Samsung Group", + "vendorclass": "android-dhcp-10", + "os_type": 1, + "type": "9", + "nickName": None, + "defaultType": "9", + "ROG": "0", + "amesh_isRe": "0", + "amesh_bind_mac": None, + "amesh_bind_band": "0", + }, + "00:00:00:00:00:49": { + "mac": "00:00:00:00:00:49", + "name": "FakeName", + "vendor": "Espressif Inc.", + "vendorclass": "Espressif Inc.", + "os_type": 2, + "type": "0", + "nickName": "FakeNickName", + "defaultType": "1", + "ROG": "0", + "amesh_isRe": "0", + "amesh_bind_mac": None, + "amesh_bind_band": "0", + }, + "00:00:00:00:00:50": { + "mac": "00:00:00:00:00:50", + "name": "FakeName", + "vendor": "Asus", + "vendorclass": "Espressif Inc.", + "type": "34", + "os_type": 0, + "nickName": "FakeNickName", + "defaultType": "24", + "ROG": "0", + "amesh_isRe": "0", + "amesh_bind_mac": None, + "amesh_bind_band": "0", + }, + "00:00:00:00:00:51": { + "mac": "00:00:00:00:00:51", + "name": "FakeName", + "vendor": "android-dhcp-10", + "vendorclass": "android-dhcp-10", + "type": "9", + "os_type": 1, + "nickName": "FakeNickName", + "defaultType": "9", + "ROG": "0", + "amesh_isRe": "0", + "amesh_bind_mac": None, + "amesh_bind_band": "0", + }, + "00:00:00:00:00:52": { + "mac": "00:00:00:00:00:52", + "name": "FakeName", + "vendor": "android-dhcp-10", + "vendorclass": "android-dhcp-10", + "type": "9", + "os_type": 1, + "nickName": None, + "defaultType": "9", + "ROG": "0", + "amesh_isRe": "0", + "amesh_bind_mac": None, + "amesh_bind_band": "0", + }, + "00:00:00:00:00:53": { + "mac": "00:00:00:00:00:53", + "name": "FakeName", + "vendor": "Raspberry Pi Foundation", + "vendorclass": "Raspberry Pi Foundation", + "type": "0", + "os_type": 0, + "nickName": None, + "defaultType": "0", + "ROG": "0", + "amesh_isRe": "0", + "amesh_bind_mac": None, + "amesh_bind_band": "0", + }, + "00:00:00:00:00:54": { + "mac": "00:00:00:00:00:54", + "name": "FakeName", + "vendor": "Raspberry Pi Foundation", + "vendorclass": "Raspberry Pi Foundation", + "type": "0", + "os_type": 0, + "nickName": "FakeNickName", + "defaultType": "0", + "ROG": "0", + "amesh_isRe": "0", + "amesh_bind_mac": None, + "amesh_bind_band": "0", + }, + "00:00:00:00:00:55": { + "mac": "00:00:00:00:00:55", + "name": "FakeName", + "vendor": "Synology Incorporated", + "vendorclass": "Synology", + "type": "4", + "os_type": 0, + "nickName": "FakeNickName", + "defaultType": "4", + "ROG": "0", + "amesh_isRe": "0", + "amesh_bind_mac": None, + "amesh_bind_band": "0", + }, + "00:00:00:00:00:56": { + "mac": "00:00:00:00:00:56", + "name": None, + "vendor": "Microsoft Corp.", + "vendorclass": None, + "type": "34", + "os_type": 0, + "nickName": None, + "defaultType": "34", + "ROG": "0", + "amesh_isRe": "0", + "amesh_bind_mac": None, + "amesh_bind_band": "0", + }, + "00:00:00:00:00:57": { + "mac": "00:00:00:00:00:57", + "name": "MSFT 5.0", + "vendor": "MSFT 5.0", + "vendorclass": "MSFT 5.0", + "type": "30", + "os_type": 2, + "sdn_idx": 0, + "online": "0", + "wireless": 2, + "is_wireless": 2, + "conn_ts": 1691951484, + "nickName": None, + "defaultType": "30", + "ROG": "0", + "amesh_isRe": "0", + "amesh_bind_mac": None, + "amesh_bind_band": "0", + }, + "00:00:00:00:00:59": { + "mac": "00:00:00:00:00:59", + "name": None, + "vendor": "Apple, Inc.", + "vendorclass": None, + "type": "0", + "os_type": 5, + "sdn_idx": 0, + "online": "0", + "wireless": 2, + "is_wireless": 2, + "conn_ts": 1698701168, + "nickName": "00:00:00:00:00:59", + "defaultType": "10", + "ROG": "0", + "amesh_isRe": "0", + "amesh_bind_mac": None, + "amesh_bind_band": "0", + }, + "00:00:00:00:00:60": { + "mac": "00:00:00:00:00:60", + "name": None, + "vendor": None, + "vendorclass": None, + "type": "0", + "os_type": 0, + "sdn_idx": 0, + "online": "0", + "wireless": 2, + "is_wireless": 2, + "conn_ts": 1699113854, + "nickName": None, + "defaultType": "0", + "ROG": "0", + "amesh_isRe": "0", + "amesh_bind_mac": None, + "amesh_bind_band": "0", + }, + } +} diff --git a/tests/test_data/rt_ax88u_merlin_388/vpn_001.content b/tests/test_data/rt_ax88u_merlin_388/vpn_001.content new file mode 100644 index 0000000..d5c6186 --- /dev/null +++ b/tests/test_data/rt_ax88u_merlin_388/vpn_001.content @@ -0,0 +1,21 @@ +vpn_server1_status = "TITLE,OpenVPN 2.6.5 arm-buildroot-linux-gnueabi [SSL (OpenSSL)] [LZO] [LZ4] [EPOLL] [MH/PKTINFO] [AEAD]>TIME,2023-11-19 15:51:05,1700405465>HEADER,CLIENT_LIST,Common Name,Real Address,Virtual Address,Virtual IPv6 Address,Bytes Received,Bytes Sent,Connected Since,Connected Since (time_t),Username,Client ID,Peer ID,Data Channel Cipher>HEADER,ROUTING_TABLE,Virtual Address,Common Name,Real Address,Last Ref,Last Ref (time_t)>GLOBAL_STATS,Max bcast/mcast queue length,0>GLOBAL_STATS,dco_enabled,0>END>"; +vpn_server2_status = "None"; +vpn_client1_status = "None"; +vpn_client2_status = "None"; +vpn_client3_status = "None"; +vpn_client4_status = "None"; +vpn_client5_status = "None"; +server1pid = "2663"; +server2pid = "-1"; +pptpdpid = "-1"; +vpn_client1_ip = "0.0.0.0"; +vpn_client2_ip = "0.0.0.0"; +vpn_client3_ip = "0.0.0.0"; +vpn_client4_ip = "0.0.0.0"; +vpn_client5_ip = "0.0.0.0"; +vpn_client1_rip = ""; +vpn_client2_rip = ""; +vpn_client3_rip = ""; +vpn_client4_rip = ""; +vpn_client5_rip = ""; + diff --git a/tests/test_data/rt_ax88u_merlin_388/vpn_001.py b/tests/test_data/rt_ax88u_merlin_388/vpn_001.py new file mode 100644 index 0000000..620c691 --- /dev/null +++ b/tests/test_data/rt_ax88u_merlin_388/vpn_001.py @@ -0,0 +1,18 @@ +"""Result of processing vpn_001.content.""" + +from asusrouter import AsusData +from asusrouter.modules.openvpn import AsusOVPNClient, AsusOVPNServer + +expected_result = { + AsusData.OPENVPN_CLIENT: { + 1: {"state": AsusOVPNClient.DISCONNECTED}, + 2: {"state": AsusOVPNClient.DISCONNECTED}, + 3: {"state": AsusOVPNClient.DISCONNECTED}, + 4: {"state": AsusOVPNClient.DISCONNECTED}, + 5: {"state": AsusOVPNClient.DISCONNECTED}, + }, + AsusData.OPENVPN_SERVER: { + 1: {"client_list": [], "routing_table": [], "state": AsusOVPNServer.CONNECTED}, + 2: {"state": AsusOVPNServer.DISCONNECTED}, + }, +} diff --git a/tests/test_devices.py b/tests/test_devices.py new file mode 100644 index 0000000..0766434 --- /dev/null +++ b/tests/test_devices.py @@ -0,0 +1,151 @@ +"""Test AsusRouter with real devices data.""" + +import importlib +import logging +import os +import re +from contextlib import contextmanager +from dataclasses import dataclass +from enum import Enum +from pathlib import Path +from typing import Any, Dict, Optional + +import pytest + +from asusrouter import AsusData +from asusrouter.modules.endpoint import Endpoint, process, read + +# Create a logger +_LOGGER = logging.getLogger(__name__) +_LOGGER.setLevel(logging.INFO) + + +class FileExtensions(Enum): + CONTENT = ".content" + PYTHON = ".py" + + +@dataclass +class DataItem: + """A class used to represent a test item.""" + + content: str + result: Dict[AsusData, Any] + endpoint: Endpoint + label: str + + def __repr__(self): + return self.label + + +@contextmanager +def log_test_data_loading(): + """Log the start and end of loading test data.""" + + _LOGGER.info("Starting to load test data") + yield + _LOGGER.info("Finished loading test data") + + +def load_content_data(device_path: Path, module_name: str) -> str: + """Load content data from a file.""" + + content_file = device_path / f"{module_name}{FileExtensions.CONTENT.value}" + with content_file.open("r", encoding="utf-8") as f: + return f.read() + + +def load_expected_result(device_path: Path, module_name: str) -> Any: + """Load expected result from a module.""" + + result_module = importlib.import_module( + f".test_data.{'.'.join([device_path.name, module_name])}", + package="tests", + ) + return result_module.expected_result + + +def load_test_item(device_path: Path, module_name: str) -> Optional[DataItem]: + """Load a single test item.""" + + try: + endpoint_name = re.match(r"(.*)_\d+", module_name).group(1) + endpoint = Endpoint[endpoint_name.upper()] + + content_data = load_content_data(device_path, module_name) + expected_result = load_expected_result(device_path, module_name) + + return DataItem( + content=content_data, + result=expected_result, + endpoint=endpoint, + label=f"{device_path.name}_{module_name}", + ) + except Exception as ex: # pylint: disable=broad-except + _LOGGER.error("Failed to load test item %s: %s", module_name, ex) + return None + + +def load_test_data() -> list[DataItem]: + """Load the test data.""" + + with log_test_data_loading(): + test_data_path = Path(__file__).resolve().parent / "test_data" + data = [] + + for device_path in test_data_path.iterdir(): + if device_path.is_dir(): + device_test_count = 0 + for content_file in device_path.glob("*.content"): + module_name = content_file.stem + + # Check if both .content and .py files exist + if ( + not (device_path / f"{module_name}.content").exists() + or not (device_path / f"{module_name}.py").exists() + ): + continue + + item = load_test_item(device_path, module_name) + data.append(item) + device_test_count += 1 + + _LOGGER.info( + "Found %s test items for device: %s", + device_test_count, + device_path.name, + ) + + _LOGGER.info("Total test items found: %s", len(data)) + return data + + +# Load the test data only once +test_data = load_test_data() + +# Create a list of ids for the test data +test_ids = [item.label for item in test_data] + + +@pytest.fixture(params=test_data, ids=test_ids) +def test_item(request): + """Yield each item in the test data.""" + + return request.param + + +def test_asusrouter(test_item: DataItem): # pylint: disable=redefined-outer-name + """ + Test the asusrouter module with the given test item. + + Args: + test_item (DataItem): The test item to use for the test. + + Raises: + AssertionError: If the actual processed data does not match the expected result. + """ + + actual_read = read(test_item.endpoint, test_item.content) + actual_processed = process(test_item.endpoint, actual_read) + + assert actual_processed == test_item.result, print(actual_processed) diff --git a/tests/tools/__init__.py b/tests/tools/__init__.py new file mode 100644 index 0000000..7016871 --- /dev/null +++ b/tests/tools/__init__.py @@ -0,0 +1 @@ +"""Tests for the tools modules.""" diff --git a/tests/tools/test_cleaners.py b/tests/tools/test_cleaners.py new file mode 100644 index 0000000..9dd7e8f --- /dev/null +++ b/tests/tools/test_cleaners.py @@ -0,0 +1,65 @@ +"""Test AsusRouter cleaners tools.""" + +from asusrouter.tools import cleaners + + +def test_clean_content(): + """Test clean_content method.""" + + # Test with BOM + content = "\ufefftest" + assert cleaners.clean_content(content) == "test" + + # Test without BOM + content = "test" + assert cleaners.clean_content(content) == "test" + + +def test_clean_dict(): + """Test clean_dict method.""" + + # Test with empty dict + data = {} + assert cleaners.clean_dict(data) == {} # pylint: disable=C1803 + + # Test with empty string + data = {"test": ""} + assert cleaners.clean_dict(data) == {"test": None} + + # Test with nested dicts + data = {"test": {"test": ""}} + assert cleaners.clean_dict(data) == {"test": {"test": None}} + + +def test_clean_dict_key_prefix(): + """Test clean_dict_key_prefix method.""" + + # Test with empty dict + data = {} + assert cleaners.clean_dict_key_prefix(data, "prefix") == {} # pylint: disable=C1803 + + # Test with empty string + data = {"prefix_test": "", "test2": ""} + assert cleaners.clean_dict_key_prefix(data, "prefix") == {"test": "", "test2": ""} + + # Test with nested dicts + data = {"prefix_test": {"prefix_test": ""}} + assert cleaners.clean_dict_key_prefix(data, "prefix") == { + "test": {"prefix_test": ""} + } + + +def test_clean_dict_key(): + """Test clean_dict_key method.""" + + # Test with empty dict + data = {} + assert cleaners.clean_dict_key(data, "test") == {} # pylint: disable=C1803 + + # Test with empty string + data = {"test": "", "test2": ""} + assert cleaners.clean_dict_key(data, "test") == {"test2": ""} + + # Test with nested dicts + data = {"test": {"test": ""}, "test2": {"test": ""}} + assert cleaners.clean_dict_key(data, "test") == {"test2": {}} diff --git a/tests/tools/test_converters.py b/tests/tools/test_converters.py new file mode 100644 index 0000000..4d3946e --- /dev/null +++ b/tests/tools/test_converters.py @@ -0,0 +1,489 @@ +"""Test AusRouter converters tools.""" + +from datetime import datetime, timedelta, timezone +from enum import Enum +from unittest.mock import patch + +import pytest + +from asusrouter.tools import converters + + +@pytest.mark.parametrize( + ("content", "result"), + [ + (None, None), # Not a string + (12, None), # Not a string + ("", None), # Empty string + (" ", None), # Empty string + ("test", "test"), # Normal string + (" test ", "test"), # Normal string + ], +) +def test_clean_string(content, result): + """Test clean_string method.""" + + assert converters.clean_string(content) == result + + +def test_flatten_dict(): + """Test flatten_dict method.""" + + nested_dict = {"a": {"b": {"c": 1}}, "d": {"e": 2}} + expected_output = {"a_b_c": 1, "d_e": 2} + assert converters.flatten_dict(nested_dict) == expected_output + + # Test with None input + assert converters.flatten_dict(None) is None + + # Test with non-dict input + assert converters.flatten_dict("not a dict") == {} # type: ignore + + # Test with exclude parameter + nested_dict = {"a": {"b": {"c": 1}}, "d": 2} + expected_output = {"a_b": {"c": 1}, "d": 2} + assert converters.flatten_dict(nested_dict, exclude="b") == expected_output + + +class EnumForTest(Enum): + """Enum class.""" + + A = 1 + B = 2 + + +def test_get_enum_key_by_value(): + """Test get_enum_key_by_value method.""" + + assert ( + converters.get_enum_key_by_value(EnumForTest, 1, EnumForTest.B) == EnumForTest.A + ) + assert ( + converters.get_enum_key_by_value(EnumForTest, 2, EnumForTest.A) == EnumForTest.B + ) + assert ( + converters.get_enum_key_by_value(EnumForTest, 3, EnumForTest.A) == EnumForTest.A + ) + with pytest.raises(ValueError): + converters.get_enum_key_by_value(EnumForTest, 3) + + +def test_is_enum(): + """Test is_enum method.""" + + class TestEnum(Enum): + """Enum class.""" + + A = 1 + B = 2 + + class NotEnum: # pylint: disable=too-few-public-methods + """Not enum class.""" + + assert converters.is_enum(TestEnum) is True + assert converters.is_enum(NotEnum) is False + + # Test with non-class input + assert converters.is_enum("not a class") is False + assert converters.is_enum(None) is False + + +def test_list_from_dict(): + """Test list_from_dict method.""" + + # Test with empty dict + assert converters.list_from_dict({}) == [] + + # Test with non-empty dict + assert converters.list_from_dict({"a": 1, "b": 2}) == ["a", "b"] + + # Test with a list + assert converters.list_from_dict(["a", "b"]) == ["a", "b"] + + # Test with non-dict input + assert converters.list_from_dict("not a dict") == [] # type: ignore + assert converters.list_from_dict(None) == [] # type: ignore + + +def test_nvram_get(): + """Test nvram_get method.""" + + # Test with empty input + assert converters.nvram_get(None) is None + + # Test with string input + assert converters.nvram_get("test") == [("nvram_get", "test")] + + # Test with list input + assert converters.nvram_get(["test1", "test2"]) == [ + ("nvram_get", "test1"), + ("nvram_get", "test2"), + ] + + # Test with other input + assert converters.nvram_get(123) == [("nvram_get", "123")] # type: ignore + + +def test_run_method(): + """Test run_method method.""" + + # Test with empty input + assert converters.run_method(None, None) is None + + # Test with non-list input + assert converters.run_method("TEST", str.lower) == "test" + + # Test with list input + assert converters.run_method("TEST", [str.lower, str.upper]) == "TEST" + + # Test with enum input + class TestEnum(Enum): + """Enum class.""" + + A = 1 + B = 2 + + assert converters.run_method(1, TestEnum) == TestEnum.A + assert converters.run_method(2, TestEnum) == TestEnum.B + assert converters.run_method(3, TestEnum) is None + + # Test with enum input with UNKNOWN + class TestEnumWithUnknown(Enum): + """Enum class.""" + + UNKNOWN = -999 + A = 1 + B = 2 + + assert converters.run_method(3, TestEnumWithUnknown) == TestEnumWithUnknown.UNKNOWN + + +@pytest.mark.parametrize( + ("content", "result"), + [ + (None, None), # Non-booleans + ("", None), + (" ", None), + ("unknown", None), + ("test", None), + (" test ", None), + (False, False), # False booleans + (0, False), + ("0", False), + ("false", False), + ("off", False), + ("disabled", False), + (True, True), # True booleans + (1, True), + ("1", True), + ("true", True), + ("on", True), + ("enabled", True), + ], +) +def test_safe_bool(content, result): + """Test safe_bool method.""" + + assert converters.safe_bool(content) == result + + +@pytest.mark.parametrize( + ("content", "result"), + [ + ("2021-01-01 ", datetime(2021, 1, 1)), # Date content + ("2021-01-01 00:00:00", datetime(2021, 1, 1)), + (None, None), # None content + ("", None), + (" ", None), + ("unknown", None), # Non-datetime content + ("test", None), + (" test ", None), + ], +) +def test_safe_datetime(content, result): + """Test safe_datetime method.""" + + assert converters.safe_datetime(content) == result + + +def test_safe_exists(): + """Test safe_exists method.""" + + assert converters.safe_exists("") is False + assert converters.safe_exists("test") is True + + +@pytest.mark.parametrize( + ("content", "result"), + [ + (1, 1.0), # Number content + (1.0, 1.0), + ("1", 1.0), + ("1.0", 1.0), + (None, None), # None content + ("", None), + (" ", None), + ("unknown", None), # Non-number content + ("test", None), + (" test ", None), + ], +) +def test_safe_float(content, result): + """Test safe_float method.""" + + assert converters.safe_float(content) == result + + +@pytest.mark.parametrize( + ("content", "base", "result"), + [ + (1, 10, 1), # Integer content + ("1", 10, 1), + (1.0, 10, 1), # Float content + ("1.0", 10, 1), + ("1.1", 10, 1), # Float content with decimal + ("1.9", 10, 1), + (None, 10, None), # None content + ("", 10, None), + (" ", 10, None), + ("unknown", 10, None), # Non-number content + ("test", 10, None), + (" test ", 10, None), + ("0x1", 16, 1), # Hex content + ("0xA", 16, 10), + ("0xFF", 16, 255), + ("0x100", 16, 256), + ("0xabc", 16, 2748), + ("0xABC", 16, 2748), + ("0x2692247c7", 16, 10353788871), + ("0x123456789ABCDEF", 16, 81985529216486895), + ], +) +def test_safe_int(content, base, result): + """Test safe_int method.""" + + assert converters.safe_int(content, base=base) == result + + +@pytest.mark.parametrize( + ("content", "result"), + [ + (None, []), # None content + ("test", ["test"]), # Single value content + (1, [1]), + (1.0, [1.0]), + (True, [True]), + (False, [False]), + ([], []), # List content + ([1, 2, 3], [1, 2, 3]), + ], +) +def test_safe_list(content, result): + """Test safe_list method.""" + + assert converters.safe_list(content) == result + + +def test_safe_list_csv(): + """Test safe_list_csv method.""" + + assert converters.safe_list_csv("test") == ["test"] + assert converters.safe_list_csv("test1,test2") == ["test1", "test2"] + + +@pytest.mark.parametrize( + ("content", "delimiter", "result"), + [ + (None, None, []), # Not a string + (1, None, []), + ({1: 2}, None, []), + ("test", None, ["test"]), # String + ("test1 test2", None, ["test1", "test2"]), + ("test1 test2", ";", ["test1 test2"]), # Wrong delimiter + ], +) +def test_safe_list_from_string(content, delimiter, result): + """Test safe_list_from_string method.""" + + assert converters.safe_list_from_string(content, delimiter) == result + + +@pytest.mark.parametrize( + ("content", "result"), + [ + (None, None), # None content + (1, 1), # Integer content + (5.0, 5.0), # Float content + ([1, 2, 3], [1, 2, 3]), # List content + ({"a": 1}, {"a": 1}), # Dictionary content + ("test", "test"), # String content + (" test ", "test"), + (" ", None), # Empty string content + ("", None), + ], +) +def test_safe_return(content, result): + """Test safe_return method.""" + + assert converters.safe_return(content) == result + + +@pytest.mark.parametrize( + ("current", "previous", "time_delta", "result"), + [ + (None, None, None, 0.0), + (1, None, None, 0.0), + (1, 1, None, 0.0), + (1, 1, 0, 0.0), + (1, 1, 1, 0.0), + (1, 1, 2, 0.0), + (1, 2, 1, 0.0), + (2, 1, 1, 1.0), + (4, 2, 2, 1.0), + ], +) +def test_safe_speed(current, previous, time_delta, result): + """Test safe_speed method.""" + + assert converters.safe_speed(current, previous, time_delta) == result + + +@patch("asusrouter.tools.converters.datetime") +def test_safe_time_from_delta(mock_datetime): + """Test safe_time_from_delta method.""" + + # Set up the mock to return a specific datetime when now() is called + mock_datetime.now.return_value = datetime(2023, 8, 15, tzinfo=timezone.utc) + + result = converters.safe_time_from_delta("48:00:15") # 48 hours, 15 seconds + expected = datetime(2023, 8, 12, 23, 59, 45, tzinfo=timezone.utc) + assert result == expected + + +def test_safe_timedelta_long(): + """Test safe_timedelta_long method.""" + + # Test with valid string + assert converters.safe_timedelta_long("01:30:15 ") == timedelta( + hours=1, minutes=30, seconds=15 + ) + assert converters.safe_timedelta_long(" 30:15:27") == timedelta( + hours=30, minutes=15, seconds=27 + ) + + # Test with invalid string + assert converters.safe_timedelta_long("invalid") == timedelta() + + # Test with None + assert converters.safe_timedelta_long(None) == timedelta() + + +def test_safe_unpack_key(): + """Test safe_unpack_key method.""" + + def test_method(content): + """Test method.""" + + return {"content": content} + + # Test with a key only + result = converters.safe_unpack_key("key") + assert result[0] == "key" and result[1] is None + + # Test with a key and a method + result = converters.safe_unpack_key(("key", test_method)) + if result[1] is not None and not isinstance(result[1], list): + assert result[0] == "key" and result[1](10) == test_method(10) + elif result[1] is not None: + assert result[0] == "key" and result[1][0](10) == test_method(10) + else: + assert result[0] == "key" and result[1] is None + + # Test with a key and a list of methods + result = converters.safe_unpack_key(("key", [test_method, test_method])) + if isinstance(result[1], list): + assert ( + result[0] == "key" + and result[1][0](10) == test_method(10) + and result[1][1](10) == test_method(10) + ) + + # Test with a key and a non-method at index 1 + result = converters.safe_unpack_key(("key", 123)) # type: ignore + assert result[0] == "key" and result[1] is None + + # Test with a tuple of one element + result = converters.safe_unpack_key(("key",)) + assert result[0] == "key" and result[1] is None + + +def test_safe_unpack_keys(): + """Test safe_unpack_keys method.""" + + def test_method(content): + """Test method.""" + + return {"content": content} + + # Test with a key, key_to_use and a method + result = converters.safe_unpack_keys(("key", "key_to_use", test_method)) + assert ( + result[0] == "key" + and result[1] == "key_to_use" + and result[2](10) == test_method(10) + ) + + # Test with a key, key_to_use and a list of methods + result = converters.safe_unpack_keys( + ("key", "key_to_use", [test_method, test_method]) + ) + assert ( + result[0] == "key" + and result[1] == "key_to_use" + and result[2][0](10) == test_method(10) + and result[2][1](10) == test_method(10) + ) + + # Test with a key and key_to_use only + result = converters.safe_unpack_keys(("key", "key_to_use")) + assert result[0] == "key" and result[1] == "key_to_use" and result[2] is None + + # Test with a key only + result = converters.safe_unpack_keys("key") + assert result[0] == "key" and result[1] == "key" and result[2] is None + + +@pytest.mark.parametrize( + ("used", "total", "result"), + [ + (5, 10, 50.0), # normal usage + (10, 10, 100.0), # normal usage + (3, 9, 33.33), # round to 2 decimals + (-1, 2, 0.0), # negative usage not allowed + (1, -2, 0.0), # negative usage not allowed + (-1, -1, 100.0), # both negative values result in positive usage + (1, 0, 0.0), # zero total usage not allowed + (0, 0, 0.0), # zero total usage not allowed + ], +) +def test_safe_usage(used, total, result): + """Test safe_usage method.""" + + assert converters.safe_usage(used, total) == result + + +@pytest.mark.parametrize( + ("used", "total", "prev_used", "prev_total", "result"), + [ + (10, 20, 5, 10, 50.0), # normal usage + (10, 20, 10, 20, 0.0), # no usage + (6, 18, 3, 9, 33.33), # round to 2 decimals + (5, 20, 10, 10, 0.0), # invalid case when current used is less than previous + (10, 20, 5, 25, 0.0), # invalid case when current total is less than previous + (5, 10, 10, 20, 0.0), # invalid case when current values are less than previous + ], +) +def test_safe_usage_historic(used, total, prev_used, prev_total, result): + """Test safe_usage_historic method.""" + + assert converters.safe_usage_historic(used, total, prev_used, prev_total) == result diff --git a/tests/tools/test_readers.py b/tests/tools/test_readers.py new file mode 100644 index 0000000..39eb4c9 --- /dev/null +++ b/tests/tools/test_readers.py @@ -0,0 +1,168 @@ +"""Test AsusRouter readers tools.""" + +import pytest + +from asusrouter.const import ContentType +from asusrouter.tools import readers + + +@pytest.mark.parametrize( + "dict1, dict2, expected", + [ + # Test non-nested dictionaries + ({"a": 1, "b": 2}, {"b": 3, "c": 4}, {"a": 1, "b": 2, "c": 4}), + # Test nested dictionaries + ( + {"a": 1, "b": {"x": 2}}, + {"b": {"y": 3}, "c": 4}, + {"a": 1, "b": {"x": 2, "y": 3}, "c": 4}, + ), + # Test with None values + ({"a": None, "b": 2}, {"a": 1, "b": None}, {"a": 1, "b": 2}), + ({"a": None}, {"a": {"b": 1}}, {"a": {"b": 1}}), + ], +) +def test_merge_dicts(dict1, dict2, expected): + """Test merge_dicts method.""" + + assert readers.merge_dicts(dict1, dict2) == expected + + +@pytest.mark.parametrize( + "content, expected", + [ + ("Test string", "test_string"), # Upper case + ( + "test with special ^$@ characters", + "test_with_special_characters", + ), # Special characters + ("snake_case", "snake_case"), # Already snake case + ], +) +def test_read_as_snake_case(content, expected): + """Test read_as_snake_case method.""" + + assert readers.read_as_snake_case(content) == expected + + +@pytest.mark.parametrize( + "headers, expected", + [ + ({"content-type": "application/json;charset=UTF-8"}, ContentType.JSON), + ({"content-type": "application/xml"}, ContentType.XML), + ({"content-type": "text/html"}, ContentType.HTML), + ({"content-type": "text/plain"}, ContentType.TEXT), + ({"content-type": "application/octet-stream"}, ContentType.BINARY), + ({"content-type": "application/unknown"}, ContentType.UNKNOWN), + ({}, ContentType.UNKNOWN), + ], +) +def test_read_content_type(headers, expected): + """Test read_content_type method.""" + + assert readers.read_content_type(headers) == expected + + +@pytest.mark.parametrize( + "content, expected", + [ + # JS from the active temperature sensors + ('curr_coreTmp_wl0_raw = "44°C";', {"curr_coreTmp_wl0_raw": "44°C"}), + # JS from the disabled temperature sensors + ( + 'curr_coreTmp_wl2_raw = "disabled";', + {"curr_coreTmp_wl2_raw": "disabled"}, + ), + # JS from the VPN + ('vpn_client1_status = "None";', {"vpn_client1_status": "None"}), + ], +) +def test_read_js_variables(content, expected): + """Test read_js_variables method.""" + + assert readers.read_js_variables(content) == expected + + +@pytest.mark.parametrize( + "content, expected", + [ + # JSON from ethernet ports + ( + '{ "portSpeed": { "WAN 0": "G", "LAN 1": "G", "LAN 2": "G", "LAN 3": "G",\ + "LAN 4": "X", "LAN 5": "X", "LAN 6": "X", "LAN 7": "X", "LAN 8": "G" },\ + "portCount": { "wanCount": 1, "lanCount": 8 } }', + { + "portSpeed": { + "WAN 0": "G", + "LAN 1": "G", + "LAN 2": "G", + "LAN 3": "G", + "LAN 4": "X", + "LAN 5": "X", + "LAN 6": "X", + "LAN 7": "X", + "LAN 8": "G", + }, + "portCount": {"wanCount": 1, "lanCount": 8}, + }, + ), + # JSON from sysinfo + ( + '{"wlc_0_arr":["11", "11", "11"],"wlc_1_arr":["2", "2", "2"],\ + "wlc_2_arr":["0", "0", "0"],"wlc_3_arr":["0", "0", "0"],\ + "conn_stats_arr":["394","56"],"mem_stats_arr":["882.34", "395.23",\ + "0.00", "52.64", "0.00", "0.00", "85343", "7.61 / 63.00 MB"],\ + "cpu_stats_arr":["2.18", "2.09", "2.03"]}', + { + "wlc_0_arr": ["11", "11", "11"], + "wlc_1_arr": ["2", "2", "2"], + "wlc_2_arr": ["0", "0", "0"], + "wlc_3_arr": ["0", "0", "0"], + "conn_stats_arr": ["394", "56"], + "mem_stats_arr": [ + "882.34", + "395.23", + "0.00", + "52.64", + "0.00", + "0.00", + "85343", + "7.61 / 63.00 MB", + ], + "cpu_stats_arr": ["2.18", "2.09", "2.03"], + }, + ), + # Test valid JSON content + ('{"key": "value"}', {"key": "value"}), + # Test empty content + (None, {}), + # Test invalid JSON content + ("not a json", {}), + ], +) +def test_read_json_content(content, expected): + """Test read_json_content method.""" + + assert readers.read_json_content(content) == expected + + +@pytest.mark.parametrize( + "content, expected", + [ + # Test valid MAC addresses + ("01:23:45:67:89:AB", True), + ("01-23-45-67-89-AB", True), + # Test invalid MAC addresses + ("01:23:45:67:89-87-65", False), + ("01-23-45-67-89", False), + ("01:23:45:67:89:ZZ", False), + (" ", False), + # Test non-string input + (1234567890, False), + (None, False), + ], +) +def test_readable_mac(content, expected): + """Test readable_mac method.""" + + assert readers.readable_mac(content) == expected diff --git a/tests/tools/test_writers.py b/tests/tools/test_writers.py new file mode 100644 index 0000000..65d0a07 --- /dev/null +++ b/tests/tools/test_writers.py @@ -0,0 +1,19 @@ +"""Test AsusRouter writers tools.""" + +import pytest + +from asusrouter.tools import writers + + +@pytest.mark.parametrize( + "content, expected", + [ + ("test", "nvram_get(test);"), + (["test1", "test2"], "nvram_get(test1);nvram_get(test2);"), + (None, None), + ], +) +def test_nvram(content, expected): + """Test nvram method.""" + + assert writers.nvram(content) == expected diff --git a/tests/util/__init__.py b/tests/util/__init__.py deleted file mode 100644 index b647a59..0000000 --- a/tests/util/__init__.py +++ /dev/null @@ -1 +0,0 @@ -"""Tests for the utilities""" diff --git a/tests/util/test_calculators.py b/tests/util/test_calculators.py deleted file mode 100644 index 092e42f..0000000 --- a/tests/util/test_calculators.py +++ /dev/null @@ -1,73 +0,0 @@ -"""Test AsusRouter calculators module""" - -import pytest - -from asusrouter.const import DATA_TOTAL, DATA_USAGE, DATA_USED -from asusrouter.util import calculators - -TEST_USAGE_IN_DICT = { - DATA_TOTAL: 54, - DATA_USED: 2, -} -TEST_USAGE_IN_DICT_RESULT = {DATA_TOTAL: 54, DATA_USED: 2, DATA_USAGE: 3.70} -TEST_USAGE_IN_DICT_2 = { - DATA_TOTAL: 77, - DATA_USED: 5, -} -TEST_USAGE_IN_DICT_RESULT_2 = {DATA_TOTAL: 77, DATA_USED: 5, DATA_USAGE: 6.49} -TEST_USAGE_IN_DICT_RESULT_3 = {DATA_TOTAL: 77, DATA_USED: 5, DATA_USAGE: 13.04} - - -def test_usage(): - """Test usage calculator""" - - # Nothing used -> no usage - assert calculators.usage(0, 0, 0, 0) == 0 - # Normal usage - assert calculators.usage(19, 117, 15, 102) == 26.67 - # Usage without prervious values - assert calculators.usage(42, 53) == 79.25 - - # Negative usage test - with pytest.raises(ValueError): - calculators.usage(10, 20, 11, 15) - with pytest.raises(ValueError): - calculators.usage(13, 20, 11, 21) - # Above 100% usage test - with pytest.raises(ValueError): - calculators.usage(2, 1) - # Zero division - with pytest.raises(ZeroDivisionError): - calculators.usage(5, 20, 1, 20) == 0 - - -def test_usage_in_dict(): - """Test usage_in_dict calculator""" - - # Tests with absolute moment data - assert calculators.usage_in_dict(TEST_USAGE_IN_DICT) == TEST_USAGE_IN_DICT_RESULT - assert ( - calculators.usage_in_dict(TEST_USAGE_IN_DICT_2) == TEST_USAGE_IN_DICT_RESULT_2 - ) - - # Tests with relative data - assert ( - calculators.usage_in_dict(TEST_USAGE_IN_DICT_2, TEST_USAGE_IN_DICT) - == TEST_USAGE_IN_DICT_RESULT_3 - ) - - -def test_speed(): - """Test speed calculator""" - - # Normal case - assert calculators.speed(13, 5, 2) == 4 - # No time_delta - assert calculators.speed(86, 49) == 0 - # Overflow test - assert calculators.speed(6, 14, 2, 16) == 4 - assert calculators.speed(19, 2, 17, 32) == 1 - - # Zero time_delta - with pytest.raises(ZeroDivisionError): - calculators.speed(174, 162, 0) diff --git a/tests/util/test_converters.py b/tests/util/test_converters.py deleted file mode 100644 index edc69c3..0000000 --- a/tests/util/test_converters.py +++ /dev/null @@ -1,106 +0,0 @@ -"""Test AsusRouter converters module""" - -from datetime import datetime, timedelta - -import pytest - -from asusrouter.util import converters - - -def test_int_from_str(): - """Test string to integer convertion""" - - # base 10 - assert converters.int_from_str("17") == 17 - assert converters.int_from_str("-42") == -42 - assert converters.int_from_str("16", 16) == 22 - # base 16 - assert converters.int_from_str("ff", 16) == 255 - assert converters.int_from_str("0xaa", 16) == 170 - # Empty string - assert converters.int_from_str(" ", 16) == 0 - - # Not a string - with pytest.raises(ValueError): - converters.int_from_str(10) - with pytest.raises(ValueError): - converters.int_from_str(94.256) - # Not an integer string - with pytest.raises(ValueError): - converters.int_from_str("4.5") - - -def test_float_from_str(): - """Test string to float convertion""" - - # Floats, ints and spaces - assert converters.float_from_str("17.69") == 17.69 - assert converters.float_from_str("-91.4 ") == -91.4 - assert converters.float_from_str(" 1") == 1.0 - # Empty string - assert converters.float_from_str(" ") == 0.0 - - # Not a string - with pytest.raises(ValueError): - converters.float_from_str(4.5) - - -def test_bool_from_any(): - """Test any to bool convertion""" - - # Numbers - assert converters.bool_from_any(0) == False - assert converters.bool_from_any(13) == True - assert converters.bool_from_any(-97.5) == True - # Strings - assert converters.bool_from_any("FaLsE") == False - assert converters.bool_from_any("trUE") == True - assert converters.bool_from_any(" 0") == False - assert converters.bool_from_any(" 1 ") == True - - # Unknown strings - with pytest.raises(ValueError): - converters.bool_from_any("00") - with pytest.raises(ValueError): - converters.bool_from_any("fake_string") - # Unknown types - with pytest.raises(ValueError): - converters.bool_from_any(datetime.utcnow()) - - -def test_none_or_str(): - """None or string convertion""" - - # Empty strings - assert converters.none_or_str("") == None - assert converters.none_or_str(" ") == None - # Usual strings - assert converters.none_or_str("The Machine") == "The Machine" - assert ( - converters.none_or_str(" You are being watched ") == "You are being watched" - ) - - # Not strings - with pytest.raises(ValueError): - converters.none_or_str(-12.4) - with pytest.raises(ValueError): - converters.none_or_str(5356295141) - with pytest.raises(ValueError): - converters.none_or_str(datetime.utcnow()) - - -def test_timedelta_long(): - """Test long timedelta convertion""" - - # Normal strings - assert converters.timedelta_long("32:15:07") == timedelta( - hours=32, minutes=15, seconds=7 - ) - # Strings with spaces - assert converters.timedelta_long(" 6:32:13 ") == timedelta( - hours=6, minutes=32, seconds=13 - ) - - # Not strings - with pytest.raises(ValueError): - converters.timedelta_long(10)