#!/usr/bin/env python3 """ymcs.py — CLI for the Yealink Management Cloud Service (YMCS) v2 API. Read-only by default; every write (add/delete/reboot/reset/RPS) is gated behind --confirm. Pairs with the `packetdial` skill for phone provisioning. ymcs.py sites|devices|accounts|sipaccounts|device-groups|device-configs firmwares|official-firmwares|models|alarms|oplogs|rps-servers|rps-devices ymcs.py devices --site # filter a list by site ymcs.py add-devices-by-mac --body '{...}' --confirm ymcs.py reboot --body '{"ids":[...]}' --confirm ymcs.py rps-add --body '{...}' --confirm ymcs.py raw POST /v2/dm/listDevices --body '{"skip":0,"limit":50,"autoCount":true}' """ import sys, os, json, argparse sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) from ymcs_client import YmcsClient, YmcsError def _emit(obj): print(json.dumps(obj, indent=2, default=str)) def _parse_body(s): if not s: return {} try: return json.loads(s) except json.JSONDecodeError as e: print(f"[ERROR] --body is not valid JSON: {e}", file=sys.stderr); sys.exit(2) def _require_confirm(args, action, detail=""): if not getattr(args, "confirm", False): print(f"[DRY RUN] Would {action}" + (f": {detail}" if detail else "")) print("Refusing to perform a write without --confirm. Re-run with --confirm.") sys.exit(0) def main(argv=None): p = argparse.ArgumentParser(prog="ymcs.py", description="Yealink YMCS v2 API client") sub = p.add_subparsers(dest="cmd", required=True) # reads for name in ["sites", "accounts", "device-groups", "device-configs", "firmwares", "official-firmwares", "models", "alarms", "oplogs", "rps-servers", "rps-devices"]: sub.add_parser(name) d = sub.add_parser("devices"); d.add_argument("--site") # writes (gated) for name in ["add-devices-by-mac", "add-devices", "del-devices", "reboot", "reset", "rps-add", "rps-del", "add-sipaccount"]: w = sub.add_parser(name); w.add_argument("--body", required=True); w.add_argument("--confirm", action="store_true") r = sub.add_parser("raw") r.add_argument("method", choices=["GET", "POST", "PUT", "PATCH", "DELETE"]) r.add_argument("path"); r.add_argument("--body"); r.add_argument("--confirm", action="store_true") args = p.parse_args(argv) try: c = YmcsClient() if args.cmd == "sites": _emit(c.list_sites()) elif args.cmd == "devices": _emit(c.list_devices(**({"siteId": args.site} if args.site else {}))) elif args.cmd == "accounts": _emit(c.list_accounts()) elif args.cmd == "device-groups": _emit(c.list_device_groups()) elif args.cmd == "device-configs": _emit(c.list_device_configs()) elif args.cmd == "firmwares": _emit(c.list_firmwares()) elif args.cmd == "official-firmwares": _emit(c.list_official_firmwares()) elif args.cmd == "models": _emit(c.models()) elif args.cmd == "alarms": _emit(c.list_alarms()) elif args.cmd == "oplogs": _emit(c.list_oplogs()) elif args.cmd == "rps-servers": _emit(c.rps_list_servers()) elif args.cmd == "rps-devices": _emit(c.rps_list_devices()) elif args.cmd == "add-devices-by-mac": b = _parse_body(args.body); _require_confirm(args, "ADD devices by MAC", json.dumps(b)[:160]); _emit(c.add_devices_by_mac(b)) elif args.cmd == "add-devices": b = _parse_body(args.body); _require_confirm(args, "ADD devices", json.dumps(b)[:160]); _emit(c.add_devices(b)) elif args.cmd == "del-devices": b = _parse_body(args.body); _require_confirm(args, "DELETE devices", json.dumps(b)[:160]); _emit(c.del_devices(b)) elif args.cmd == "reboot": b = _parse_body(args.body); _require_confirm(args, "REBOOT devices", json.dumps(b)[:160]); _emit(c.device_reboot(b)) elif args.cmd == "reset": b = _parse_body(args.body); _require_confirm(args, "FACTORY-RESET devices", json.dumps(b)[:160]); _emit(c.device_reset(b)) elif args.cmd == "rps-add": b = _parse_body(args.body); _require_confirm(args, "RPS add devices", json.dumps(b)[:160]); _emit(c.rps_add_devices(b)) elif args.cmd == "rps-del": b = _parse_body(args.body); _require_confirm(args, "RPS delete devices", json.dumps(b)[:160]); _emit(c.rps_delete_devices(b)) elif args.cmd == "add-sipaccount": b = _parse_body(args.body); _require_confirm(args, "ADD SIP account to device", json.dumps(b)[:160]); _emit(c.add_sip_account(b)) elif args.cmd == "raw": b = _parse_body(args.body) if args.body else None path = args.path # Recover from Git-Bash MSYS path-conversion (a leading /v2/... arg gets # rewritten to C:/Program Files/Git/v2/...). Cut back to the API path. if "/v2/" in path: path = path[path.index("/v2/"):] if args.method != "GET": _require_confirm(args, f"{args.method} {path}", json.dumps(b or {})[:160]) _emit(c.request(args.method, path, body=b)) else: p.error(f"unknown command {args.cmd}") except YmcsError as exc: print(f"[ERROR] {exc}", file=sys.stderr) sys.exit(1) if __name__ == "__main__": main()