A tiny, unofficial command-line client for QNAP QSS-managed switches.
Reverse engineered API for a QSS switch. It's dangerous. Don't even think of using this. You have been warned.
That warning is only half a joke. QSS switches have no SSH and (on at least some models) no SNMP toggle in the GUI, so there's no supported way to script them. This talks to the same private REST API the QSS web UI uses, which I worked out by reading the UI's own JavaScript. It is not documented, not supported, and not blessed by QNAP. It happens to work. It might stop working the next time you update firmware. The write commands can absolutely break your switch. Proceed accordingly.
qss.py is a single Python file (standard library only) that:
- logs in and gets a bearer token, then
- reads telemetry: system info, per-port PoE draw, port link status, temps/fans, LLDP, and
- can hit any
/api/v1endpoint for reads or writes, so you're not limited to the handful of friendly commands.
It is AI-agnostic on purpose: it's just a CLI and a documented API. If you want it as a "skill" / tool for your assistant, point your favorite AI (Claude Code, etc.) at qss.py and this README and ask it to wrap it. That's the spirit, build your own.
- Not official, not affiliated with QNAP.
- Not tested across the QSS family. Validated only on a
QSW-M2106PR-2S2T, firmware1.2.1(build20250218). It very likely generalizes (the/api/v1+ bearer-JWT shape looks common to QSS), but that's unverified. PRs confirming other models welcome. - Not a security boundary. See the security notes below.
Needs Python 3.8+. The core has no dependencies.
git clone https://github.com/gothamsound/qss-switch-api.git
cd qss-switch-api
chmod +x qss.pyPyYAML is optional, and only if you want to use a YAML config file:
pip install pyyamlCredentials resolve in this order: CLI flag → environment variable → YAML config → interactive prompt.
Easiest, leaves nothing on disk:
export QSS_HOST=192.168.0.2
export QSS_USER=admin
export QSS_VERIFY_TLS=0 # QSS ships a self-signed cert; 0 skips verification on your LAN
# password is prompted (not echoed) if you don't set QSS_PASSWORD
qss.py poeOr a config file. Copy config.example.yaml to config.yaml (gitignored) and chmod 600 it, because it holds an admin password in plaintext:
cp config.example.yaml config.yaml
chmod 600 config.yaml
$EDITOR config.yaml
qss.py --config config.yaml portsqss.py login # test auth, print your privilege level
qss.py info # hostname, mac, contact, location
qss.py board # model, serial, port count
qss.py sensors # switch temp + fan rpm
qss.py poe # per-port watts / state / PD class / bt mode
qss.py ports # per-port up|down / speed / duplex
qss.py lldp # LLDP neighbors
qss.py get /ports/statistics # GET any endpoint, raw JSON
qss.py endpoints # list the endpoints discovered in the UIAdd --json to the table commands for raw output. Example PoE table:
port watts state class mode
1 0 noPoweredDeviceDetected 12 poePlusDot3bt
2 0 noPoweredDeviceDetected 12 poePlusDot3bt
...
There are no hand-holding write subcommands by design, you decide how to tie that knot. Use call with the method, path, and a JSON body. Any non-GET requires --yes:
# read first to learn the shape
qss.py get /poe/port/status
# then write (example shape only; confirm the body against your firmware)
qss.py call PUT /poe/port --data '{"idx":["3"], ...}' --yescall can reach the whole API, including reboot, config, and firmware paths. There is intentionally no guard beyond --yes. If you reconfigure your VLANs out from under yourself or brick the box, that's on you.
POST /api/v1/users/loginwith{"username": "...", "password": "<base64 of the plaintext>"}. The "encryption" you'll see in the web UI (ezEncode) is plain base64. It is encoding, not encryption, so use HTTPS, never the:80endpoint.- The response carries a JWT in
.result. Its payload includes yourPrivilege(15 = full admin = read and write). - Every later call sends
Authorization: Bearer <jwt>. Responses are{"error_code": 200, "error_message": "OK", "result": ...}(note:error_codemirrors HTTP, so 200 = success, not 0).
- The password is base64-encoded in transit, which is not encryption. Always use HTTPS; never port 80.
- A login grants an admin token. Treat the switch like any admin surface: trusted network only.
config.yamlholds a plaintext admin password. It's gitignored;chmod 600it, and prefer environment variables or the interactive prompt where you can.- TLS verification is on by default. QSS switches use a self-signed cert, so you'll need
--insecure(orverify_tls: false) on a LAN you trust.
It works on one switch on one firmware. If you confirm another QSS model, find an endpoint, or fix a break after a firmware update, open an issue or PR with the model and firmware version.
MIT. See LICENSE. Provided as-is, with no warranty; see the warning at the top, which was not a joke.