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}{key}>\n"
+ content += f" {group}>\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)