CudyPy is an unofficial Python wrapper for Cudy's local LuCI app RPC API. See compatibility and verification coverage for tested models, firmware and verification limits.
AsyncCudyRouter provides native asyncio requests for every named router
operation; CudyRouter provides the same synchronous API. Start with the
API reference for signatures and examples.
Use router.get_*() for object-based reads: system/client models,
nested response records and explicit page objects. See the
model reference for return types, fields and firmware-specific
extensions.
From the repository root, with Python 3.10 or later:
python3 -m venv .venv
. .venv/bin/activate
python -m pip install -e ".[async]"See INSTALL.md for development and build instructions. The runnable async example accepts a password or existing session token from environment variables.
Set CUDY_ROUTER_URL and CUDY_ROUTER_TOKEN in your local environment.
Use the token value from the router's auth query parameter or sysauth
cookie, not an entire Cookie header. Each router needs its own token.
import asyncio
import os
from cudypy import AsyncCudyRouter
async def main():
async with AsyncCudyRouter(
os.environ["CUDY_ROUTER_URL"],
auth_token=os.environ["CUDY_ROUTER_TOKEN"],
) as router:
status, wan, devices = await asyncio.gather(
router.get_system_status(),
router.get_interface_status("wan"),
router.get_devices(),
)
print(status.model, wan.is_up, len(devices))
asyncio.run(main())Token sessions do not need mDNS, a device ID, or your password. Expired tokens
raise CudyAuthError; obtain a fresh token and create a new client.
Closing a client only closes local resources; it does not log out the browser
or change router configuration.
With a configured synchronous router (synthetic values shown):
>>> status = router.get_system_info()
>>> type(status).__name__
'SystemStatus'
>>> status.model
'Example router'
>>> status.uptime_seconds
3600
>>> status.memory.available
64000
>>> status.board_name is None
True
>>> status.raw["uptime"]
'3600'Resource counters retain firmware-native units; a missing memory object is None.
See the model reference for other response types.
import asyncio
import os
from cudypy import AsyncCudyRouter
async def main():
async with AsyncCudyRouter(
os.environ["CUDY_ROUTER_URL"],
password=os.environ["CUDY_ROUTER_PASSWORD"],
) as router:
if not await router.authenticate():
raise RuntimeError("Router authentication failed")
print((await router.get_system_status()).firmware)
asyncio.run(main())Without an explicit salt, the client discovers the router's salt over mDNS. Use a literal router IP for mDNS matching.
authenticate() retains the prototype's boolean result for password login
failures. API reads authenticate on demand and raise CudyAuthError when
login fails. Forced login without a password raises CudyAuthError.
Only source-confirmed reads retry once after an authentication rejection when
a password is available. Transport errors and arbitrary RPC methods are never
replayed.
Both clients return models from ordinary getters. The model reference lists all return types.
Typed readers return snapshots: accessing their attributes does not send network
requests. Missing values remain None; .raw retains firmware-specific fields.
See system-status fields and units
for nested resource objects. Use .raw or call_api() for original wire data.
| Method | Result |
|---|---|
get_system_status() |
SystemStatus with nested resource snapshots |
get_system_info() |
SystemStatus (same as get_system_status()) |
get_devices() |
List of Device instances |
get_network_status(interface="wan") |
InterfaceStatus |
get_supported_features() |
Structured firmware declarations |
get_ethernet_status() |
List of EthernetPort |
get_mesh_clients() |
Structured firmware-dependent mesh result |
get_traffic_stats() |
Structured firmware-dependent traffic result |
get_online_devices() |
Devices passing the legacy inactivity heuristic |
get_wifi_devices(), get_ethernet_devices() |
Devices filtered by interface |
get_device_by_mac(mac), get_device_by_ip(ip) |
First matching device or None |
get_device_by_hostname(name, case_sensitive=False) |
First matching hostname or fallback device name |
call_api(method, params=None, retry_auth=True) |
Full RPC envelope, including result |
reboot() |
Sends a reboot request; disruptive, tested only with mocks |
Each filtering/search method fetches a new device list. Fetch once and filter locally when taking a consistent snapshot. Use one client per router and do not share a client across threads.
Raw results preserve firmware-specific keys. Do not assume firmware_version
or any other undocumented key exists. Unsupported methods raise CudyAPIError
with a numeric RPC code when supplied by the router. For example, the tested
WR3000H firmware rejects net.traffic_stat with code == -32601. Other builds
or modes may lack WAN or mesh features. See
the RPC contract.
call_api is an advanced escape hatch: it can invoke mutations. Inspect the
firmware or APK payload before using it. The unit suite mocks mutation calls;
it never sends them to hardware.
Additional reads for Wi-Fi schedules, WDS/WPS status, VPN, work modes and
per-client details/limits are described in feature reads.
Source-backed client naming, Internet blocking and rate-limit writes are
described in client controls. These controls are
offline-tested only; read the side effects and no-replay guarantees before use.
Typed wireless-section reads and the advanced nested Wi-Fi write API are
described in wireless configuration, including credential
handling and firmware compatibility limitations.
See device semantics for optional activity state,
elapsed duration, and unresolved counter direction/reset behavior.
Typed Ethernet-port status is available through get_ethernet_ports().
The parser accepts native macaddr/ipaddr and legacy mac/ip fields,
signal (dBm) or rssi (converted to dBm by subtracting 100), and string/integer
boolean values. Unknown fields are preserved in Device.raw (excluded from repr).
Malformed device-list envelopes raise errors instead of appearing as an empty
network. get_devices() follows inclusive ranges if the first response reports
more clients than it returns. Counts changing during pagination or duplicate
clients raise an error so the caller can retry a fresh read.
is_online currently uses the prototype's inactive < 30 heuristic. Missing
or invalid inactivity stays None (unknown); this is not definitive evidence
of reachability. online is interpreted as elapsed seconds for
connected_since. Firmware-reported upspeed/downspeed values are retained;
formatting uses bytes/s with explicit binary prefixes (KiB/s, MiB/s), matching
the APK's interpretation of the raw values.
inbytes/outbytes retain the prototype's receive/send mapping, whose viewpoint
also needs hardware verification.
All library errors derive from CudyAPIError; authentication and discovery have
CudyAuthError and CudyDiscoveryError subclasses. Explicitly missing methods
raise CudyUnsupportedError (RPC code -32601). HTTP failures, invalid JSON,
malformed RPC envelopes, and invalid result shapes raise errors. Request error
messages omit URLs and tokens. Requests do not follow redirects or use
environment proxies. HTTPS certificate verification remains enabled.
If requests time out, check that this machine can reach the configured router origin and that local routing, VPN or firewall rules allow the connection.
The client does not configure global logging. Avoid enabling third-party HTTP wire/debug logging when handling tokens.
The optional Cudy Monitor dashboard collects status and client rates in the background and stores history in SQLite. It provides time-range graphs, device history, router health, an event timeline and CSV exports. The FastAPI/React application runs locally and is separate from the installed library.
Run offline tests from this directory:
python -m pip install -e ".[dev]"
python -m pytestThe suite blocks unmocked HTTP access. Live compatibility checks are separate and require explicit per-router credentials. No factory resets, firmware upgrades, configuration writes, scans, WPS, or reboots are part of verification.
Licensed under MIT. Not affiliated with Cudy.