Skip to content

paleophyte/pyskinny

Repository files navigation

pyskinny — dusty-but-useful (maybe) CallManager tooling for Python

Have you ever needed to use the SCCP (Skinny) protocol in your Python project? Probably not.
Looking for automation tools for legacy Cisco CallManager? Doubtful.
Didn’t IP Telephony die like 10 years ago? Allegedly.
But on the off chance you’re here for exactly that… you’ve come to the right place.

pyskinny includes both a SCCP Client and a grab-bag of practical scripts for poking at old-school CUCM/CallManager (4.x): AXL v1 SOAP, SQL via AXL, real-time snapshots via the RTMT ASTIsapi endpoints, and direct 79xx phone control (screenshots + button presses).


What’s inside

  • SCCP softphone client (79xx softkeys + CM2 button-template phones)
    • Console UI (run_console), Cisco-style CLI, macro/IVR mode
    • Hold/resume, blind & consulted transfer, conference (lab-tested on cm2 + cm31/33/41/43)
    • Default RTP TX is silence with local RX monitor; --rtp-mic / --rtp-tone for troubleshooting
  • CallManager 4.1 AXL v1 SOAP helpers
    • listPhoneByName / listPhoneByDescription
    • executeSQLQuery with a smart rewrite to safely expand Device.* (avoids the XML LOB column)
  • AST / “RIS-ish” device snapshot
    • Uses ASTIsapi.dll?OpenDeviceSearch (works on 4.x; TLSv1 & NTLM friendly)
  • Phone control (79xx era)
    • /CGI/Screenshot (decodes CiscoIPPhoneImage → PNG)
    • /CGI/Execute (dial digits, softkeys, navigation, hardkeys)
  • JSON everywhere
    • --json, --jsonl, --pretty

Tests

Unit tests (no CallManager required):

pip install -e ".[dev]"
pytest -m "not integration" -v --no-audio

Lab docs

Topic Doc
Simulator scenarios (IVR, 3 phones, admin reset) docs/lab-cookbook.md
CM 3.x labs (cm31, cm33 — 7960/7970) docs/lab-cm3x.md
Softkey hold/resume (cm31/33/41/43) docs/lab-softkey-hold.md
CM2 button phones (Stimulus hold/transfer) docs/lab-cm2-buttons.md

CI (GitHub Actions)

Unit tests (.github/workflows/test.yml): runs on every push/PR to main on Ubuntu with Python 3.11 and 3.12. Command: pytest -m "not integration" -v --no-audio.

Integration tests (.github/workflows/integration.yml): manual workflow_dispatch on a self-hosted runner in the lab network. Defaults to all five labs (cm2, cm31, cm33, cm41, cm43) via tests/test_integration_live.py; workflow inputs override server IPs and shared MACs.

Integration (live CM labs)

# All labs (built-in server IPs — override with PYSKINNY_CM41_SERVER etc.)
$env:PYSKINNY_INTEGRATION_LABS = "cm2,cm31,cm33,cm41,cm43"
$env:PYSKINNY_SKIP_TFTP = "1"
pytest tests/test_integration_live.py -m integration -v --no-audio

# Single lab (pytest marker — no env var needed)
pytest tests/test_integration_live.py -m "integration and cm2" -v --no-audio

# Or limit labs via env (PowerShell: set on the same line, or export before pytest)
$env:PYSKINNY_INTEGRATION_LABS = "cm2"; pytest tests/test_integration_live.py -m integration -v --no-audio
Lab Default IP Model Identity
cm2 10.0.0.11 Virtual30SPplus pyskinny01–03
cm31 10.0.0.181 7960 SEP + MAC
cm33 10.0.0.182 7970 SEP + MAC
cm41 10.0.0.180 7970 SEP + MAC
cm43 100.69.0.100 7970 SEP + MAC

Shared MACs: 222233334444446. Disable a lab: PYSKINNY_CM33_DISABLE=1.

Integration scenarios (each lab, three endpoints where noted): register/unregister, outbound connect/hangup, hold/resume, blind transfer, consulted transfer.

# Examples
pytest tests/test_integration_live.py -m "integration and cm2" -v --no-audio
pytest tests/test_integration_live.py -k "hold or blind or consulted" -m integration -v --no-audio

Diagnostics (against live CM): python -m utils.dump_softkeys --config, python -m utils.dump_buttons --config (CM2).

Only one SCCP client can hold a device/MAC at a time — stop consoles before running integration tests.

Simulator tests (no CUCM):

pytest tests/test_simulator.py -v
pytest tests/test_cucm_capture_regression.py -v   # wire format vs cm_cap / cm_call pcaps

Regenerate committed capture fixtures after updating pcaps: python -m utils.extract_cucm_capture_fixtures


Requirements

  • Python 3.11+ (tested on 3.12)
  • macOS/Linux (Windows likely fine)
  • System curl (for ancient TLS fallbacks)
  • Install: pip install . or pip install -e ".[dev]" from a clone (see Quick start)
  • Note: Tested only with CallManager 4.1(3). Will it work on other versions? Unlikely.

Quick start

Install from a clone (editable, best for development):

python -m venv .venv
source .venv/bin/activate   # Windows: .venv\Scripts\activate
pip install -e ".[dev]"

Or install runtime only:

pip install .

After install, console entry points are on your PATH:

Command Same as
pyskinny-console python -m examples.run_console
pyskinny-cli python -m examples.run_cli
pyskinny-macro python -m examples.run_macro
pyskinny-sim python -m examples.run_simulator
pyskinny-tftp-relay python -m simulator.tftp_relay
pyskinny-phone python -m tools.phone
pyskinny-callmanager python -m tools.callmanager

python -m examples.* and python -m tools.* still work after install.

examples/run_macro.py

Macro-mode CLI SCCP Client

# Using a macro file
python -m examples.run_macro --server <server_ip_or_hostname> --mac <device_mac_address> --model <device_model_number> --macro-file examples/ivr.macro
2025-09-05 11:35:13,643 [MESSAGE] ui.macro_cli          : Executing: ON_DISCONNECT ['GOTO', 'TOP']
2025-09-05 11:35:13,643 [MESSAGE] ui.macro_cli          : Executing: WAIT_CALL ['0', 'RING']
2025-09-05 11:35:13,643 [MESSAGE] ui.macro_cli          : Press 'q' to quit
2025-09-05 11:35:25,031 [MESSAGE] ui.macro_cli          : Executing: SOFTKEY ['Answer']
2025-09-05 11:35:25,534 [MESSAGE] ui.macro_cli          : Executing: GOTO ['MENU']
2025-09-05 11:35:25,534 [MESSAGE] ui.macro_cli          : Executing: PLAY ['media/main_menu.wav']
2025-09-05 11:35:25,542 [MESSAGE] ui.macro_cli          : Executing: WAIT_DIGIT ['0']
2025-09-05 11:35:34,450 [MESSAGE] ui.macro_cli          : Executing: SWITCH ['last_digit', '1:SERVICE;2:SUPPORT;3:EXT_DIAL;9:MENU;DEFAULT:MENU']
2025-09-05 11:35:34,450 [MESSAGE] ui.macro_cli          : Executing: PLAY ['media/customer_service.wav']
2025-09-05 11:35:34,454 [MESSAGE] ui.macro_cli          : Executing: WAIT_DIGIT ['0']
2025-09-05 11:35:45,351 [MESSAGE] ui.macro_cli          : Executing: WAIT_CALL ['0', 'RING']
2025-09-05 11:35:45,351 [MESSAGE] ui.macro_cli          : Press 'q' to quit

# Using a macro from the CLI
python -m examples.run_macro -vvvv --server <server_ip_or_hostname> --mac <device_mac_address> --model <device_model_number> --macro "WAIT 2,CALL 1001,HOLD,WAIT 2,RESUME,WAIT 5,TRANSFER 1002,END"
2025-09-05 11:37:38,817 [MESSAGE] ui.macro_cli          : Executing: WAIT ['2']
2025-09-05 11:37:40,822 [MESSAGE] ui.macro_cli          : Executing: CALL ['1001']
2025-09-05 11:37:43,344 [MESSAGE] ui.macro_cli          : Executing: WAIT ['10']
2025-09-05 11:37:53,349 [MESSAGE] ui.macro_cli          : Executing: SOFTKEY ['EndCall']

examples/run_cli.py

Cisco-esque CLI SCCP Client

python -m examples.run_cli
Press '?' for help. Type 'exit' to quit.
phone# set server 10.0.0.180
server = 10.0.0.180

phone# set mac 222233334444
mac = 222233334444

phone# set model 7970
model = 7970

phone# set auto_connect true
auto_connect = True

phone# save
phone# show config
server: 10.0.0.180
mac:    222233334444
model:  7970
auto_connect: True

phone# connect
phone# phone call 1006
Calling 1006 ...

phone# phone hold
Hold

phone# phone resume
Resume

phone# phone transfer 1007
Blind transfer to 1007 ...

phone# show calls
Line   CallId    CallType      CallState      Time        FromNum      FromName        ToNum        ToName         
1      16777221  OutBoundCall  Connected      2 seconds   1003         Python          1006         Python7971     

phone# phone send softkey EndCall
Softkey EndCall (line 1, call 0)

phone# exit

examples/run_console.py

Console softphone UI

# 79xx (softkeys)
pyskinny-console --server 10.0.0.180 --mac 222233334444 --model 7970

# CM2 button phone (device name, not MAC)
pyskinny-console --server 10.0.0.11 --device pyskinny01 --model Virtual30SPplus

# Optional: mini web UI + RTP flags (see utils/cli_media.py)
pyskinny-console --config --web-port 8766

Keyboard (console)

Key Action
0-9 * # DTMF / dial
Space Hook on/off
h Hold / resume toggle
t Transfer (dial target, then t again to complete)
e or F-key EndCall Hang up
F1–F8 Softkeys (79xx) or button-template actions (CM2)
q Quit (sends Unregister)

Audio: hear the remote party by default (RX monitor). TX is silence unless --rtp-mic, --rtp-tone, or --rtp-loopback. CM2 may report compression_type=160366308; that is mapped to G.711 and does not affect listen-only calls.

tools/callmanager.py

List phones via AXL

python tools/callmanager.py --server <server_ip_or_hostname> --user administrator --pass <windows_admin_password> --mode name --pattern 'SEP%' --json --pretty
[
    {
        "pkid": "b4fa8ba2-ccc1-419b-9c3b-a447d0ab3604",
        "name": "SEP333344445555",
        "product": "Cisco 7971",
        "model": "Cisco 7971"
    }
]

Run SQL safely (auto-expands Device.*)

python tools/callmanager.py --server <server_ip_or_hostname> --user administrator --pass <windows_admin_password>   --sql "SELECT d.*, n.DNOrPattern
         FROM Device d
         JOIN DeviceNumPlanMap m ON m.fkDevice = d.pkid
         JOIN NumPlan n ON n.pkid = m.fkNumPlan
         ORDER BY d.Name" --pretty
[
  {
    "pkid": "{B4FA8BA2-CCC1-419B-9C3B-A447D0AB3604}",
    "Name": "SEP333344445555",
    "Description": "SEP333344445555",
    "tkModel": "119",
    "tkDeviceProtocol": "0",
    "tkProtocolSide": "1",
    "SpecialLoadInformation": "",
    "fkDevicePool": "{F71F52BC-B6C9-4968-8A86-DCDA3EEC3DD7}",
    "fkPhoneTemplate": "{9C9F4E1E-7879-4AD0-842F-A0F253864260}",
    "AssocPC": "",
    "fkCallingSearchSpace": "{96B5C16D-68B7-41FF-960F-36990DB223EC}",
    "CtiID": "1942436578",
    "tkClass": "1",
    "AddOnModules": "0",
    "fkProcessNode": "",
    "DefaultDTMFCapability": "0",
    "fkLocation": "{C8185284-D1D9-4FD5-9D7B-6ED6BB0F646C}",
    "tkProduct": "119",
    "DialPlanWizardGenID": "",
    "DeviceLevelTraceFlag": "0",
    "LoginUserid": "",
    "LoginTime": "",
    "AllowHotelingFlag": "0",
    "tkDeviceProfile": "0",
    "ikDevice_DefaultProfile": "",
    "fkMediaResourceList": "{B2E1E3C2-5198-47FC-9F44-B898CD542D2C}",
    "UserHoldMOHAudioSourceID": "0",
    "NetworkHoldMOHAudioSourceID": "0",
    "LoginDuration": "",
    "Unit": "0",
    "SubUnit": "0",
    "VersionStamp": "{1EF073B9-5898-4109-B1C7-96F4969B5A4C}",
    "tkCountry": "",
    "ikDevice_CurrentLoginProfile": "",
    "tkUserLocale": "",
    "tkProduct_Base": "",
    "fkCallingSearchSpace_AAR": "",
    "fkAARNeighborhood": "",
    "fkSoftkeyTemplate": "",
    "retryVideoCallAsAudio": "1",
    "RouteListEnabled": "",
    "fkCallManagerGroup": "",
    "tkStatus_MLPPIndicationStatus": "2",
    "tkPreemption": "2",
    "MLPPDomainID": "-1",
    "tkStatus_CallInfoPrivate": "2",
    "tkStatus_BuiltInBridge": "2",
    "tkQSIG": "4",
    "tkDeviceSecurityMode": "0",
    "V150ModemRelayCapable": "0",
    "tkNetworkLocation": "0",
    "ignorePI": "0",
    "tkPacketCaptureMode": "0",
    "PacketCaptureDuration": "60",
    "AuthenticationString": "",
    "tkAuthenticationMode": "1",
    "tkCertificateStatus": "1",
    "tkKeySize": "2",
    "tkCertificateOperation": "1",
    "UpgradeFinishTime": "",
    "tkCertificate": "0",
    "msrepl_tran_version": "{4895266D-F371-49FA-8818-12CAC83FE7C7}",
    "DNOrPattern": "1006"
  }
]

Tip: --no-rewrite disables the safety net if you really want raw SQL.

Real-time “registered devices” snapshot (ASTIsapi)

python tools/callmanager.py --server 10.0.0.180   --user administrator --pass <windows_admin_password> --ris --pretty
{
  "node": "CUCM4",
  "totalDevices": 1,
  "devices": [
    {
      "name": "SEP333344445555",
      "ip": "192.168.100.195",
      "dirNumber": "1006",
      "status_enum": 2,
      "status": "Unregistered",
      "model_enum": 119,
      "model": "Cisco 7971",
      "product_enum": 119,
      "product": "Cisco 7971",
      "perfmonObject": 2,
      "timestamp_raw": "1757065587",
      "timestamp_iso": "2025-09-05T09:46:27Z"
    }
  ]
}

Argument summary

  • General:
    • --server <ip_or_hostname> --user administrator --pass <windows_admin_password>
  • AXL listing:
    • --mode {name|description} , --pattern 'SEP%'
  • SQL:
    • --sql "SELECT …", --no-rewrite (optional)
  • AST/RIS-ish:
    • --ris
  • Output:
    • --json (array), --jsonl (one object per line), --pretty

tools/phone.py

Phone screenshot + button presses (79xx)

# Screenshot (auto-detects CiscoIPPhoneImage, writes PNG)
python tools/phone.py --phoneip 10.0.0.71 --user <username> --pass <password> --output screenshot.png

# Dial a number
python tools/phone.py --phoneip 10.0.0.71 --user <username> --pass <password> --dial 1001

# Press keys/softkeys/navigation
python tools/phone.py --phoneip 10.0.0.71 --user <username> --pass <password> --keys "123#"
python tools/phone.py --phoneip 10.0.0.71 --user <username> --pass <password> --softkey 1
python tools/phone.py --phoneip 10.0.0.71 --user <username> --pass <password> --nav down

Enable Web Access = Enabled on the phone in CUCM.
In order for auth to work, you'll need to associate a user to the device in CallManager, then you can authenticate with those user credentials.


tools/cme.py

Read CallManager Express configuration and make JSON backup

# Extract voice related configuration parameters from CME and store in JSON file. Specify DN range to build database of used DNs.
python tools/cme.py --host <cme_ip_or_hostname> --username <router_username> --password <router_password> --transport telnet collect -o <filename.json> --dn-start 9000 --dn-end 9999
  
# Add a new phone to CME. Dry run will show you the configuration that will be added.
python tools/cme.py --host <cme_ip_or_hostname> --username <router_username> --password <router_password> --transport telnet add-phone --json <filename.json> --mac 4444.5555.6666 --model 7970 --dry-run

# Add a new phone to CME. Actually add the configuration and save it.
python tools/cme.py --host <cme_ip_or_hostname> --username <router_username> --password <router_password> --transport telnet add-phone --json <filename.json> --mac 4444.5555.6666 --model 7970 --commit

Compatibility notes

  • Targeted at CUCM/CallManager 4.x (Windows/IIS).
  • AXL WSDL: /CCMAPI/AXL/V1/AXLAPI.wsdl
  • ASTIsapi: /ast/ASTIsapi.dll (OpenDeviceSearch, GetAlertSummaryList, etc.)
  • Phone models tested: 7940/7960 (others likely work if they expose /CGI/*).

Troubleshooting

  • “XML is a large text column…”: don’t SELECT * FROM Device; the tool auto-expands safe columns or pick columns explicitly.
  • TLS/SSL errors (EOF / handshake): these boxes speak TLS1.0 + antique ciphers. The scripts fall back to:
    • forcing TLSv1 in requests, and if needed,
    • shelling out to curl --tlsv1 (and --ntlm).
  • Unauthorized HTML instead of XML: include the domain (CUCM4\administrator) for NTLM, and use the double-? URL.

Skinny CallManager simulator

A minimal SCCP server that accepts any device registration and auto-assigns DNs starting at 1000 (configurable). Enough of the Skinny handshake is implemented for pyskinny clients to register and show a line.

# Terminal 1 — simulator (listens on TCP 2000)
python -m examples.run_simulator -v

# Terminal 2 — softphone aimed at the simulator
python -m examples.run_cli
phone# set server 127.0.0.1
phone# set mac AABBCCDDEEFF
phone# set model 7970
phone# set auto_connect true
phone# connect

Options: --port, --dn-start, --host, --name, --no-tftp, --tftp-port, --tftp-root, --advertise-host, --provision MAC, --auto-answer MAC, --auto-answer-all, --ivr-dn, --admin-port (default 8090, web UI for Reset/Restart/bulk actions), --rtp-sim-peer.

Full lab walkthrough: docs/lab-cookbook.md (three consoles, IVR macro, admin reconnect, second call while on hold).

TFTP configs

The simulator runs an embedded TFTP server (via tftpy) and serves:

File Purpose
XMLDefault.cnf.xml Fallback — points phones at the simulator's Skinny port
SEP<MAC>.cnf.xml Per-device file with auto-assigned DN (created on first TFTP or Skinny register)
Ringlist.xml Minimal empty ring list (seeded; optional PCM rings from CUCM)

Optional TFTP files from CUCM — Hardware phones (especially 7912/7905) may also request locale files under United_States/ (gkdefault.cfg, gk<MAC>, etc.) and ring PCM files referenced by Ringlist.xml. These are not required for register and call if SEP<MAC>.cnf.xml was served successfully. Copy them from your CallManager TFTP directory when you want full ring menus or legacy GK profiles:

# Example: mirror CUCM TFTP into a folder, then point the simulator at it
python -m examples.run_simulator -v --advertise-host 10.102.172.11 --tftp-root D:\cucm-tftp-mirror

On CUCM, files usually live under the publisher’s TFTP path (OS Admin → Software Upgrades → TFTP file management, or the tftp folder on the server). Copy Ringlist.xml, United_States\, and any .raw/.pcm ring files the phone requests.

Port 69 is the Cisco default but usually requires Administrator (Windows) or root (Linux). Run the simulator on 6969 (no admin) and use the TFTP relay when a hardware phone must talk to port 69:

# Terminal A — elevated (Administrator)
python -m simulator.tftp_relay -v

# Terminal B — normal user
python -m examples.run_simulator -vv --advertise-host 10.102.172.11 --tftp-port 6969 --tftp-root simulator/tftp_assets

The relay listens on 0.0.0.0:69 and forwards each TFTP transfer to 127.0.0.1:6969. Phones keep using CallManager / TFTP address 10.102.172.11 (port 69).

If you start the simulator without the relay and port 69 is unavailable, it automatically falls back to 6969 and logs a warning. pyskinny clients can use tftp_port=6969 on PhoneState in that case.

Force a specific port (no fallback): --tftp-port 6969.

Pre-provision a MAC before a physical phone boots (TFTP happens before Skinny TCP):

python -m examples.run_simulator --provision 222233334444 --provision 222233334445

Use --advertise-host when binding 0.0.0.0 so phone XML contains the lab IP phones can reach (not 0.0.0.0).

Calls between two phones

Two pyskinny clients registered to the simulator can call each other by DN (auto-assigned from 1000). The simulator routes on dialed digits, rings the callee, and connects on Answer (or off-hook while ringing).

# Terminal 1
python -m examples.run_simulator -v

# Terminal 2 — phone A (gets DN 1000)
python -m examples.run_cli
phone# set server 127.0.0.1
phone# set mac AABBCCDDEE01
phone# set model 7970
phone# connect
phone# show config

# Terminal 3 — phone B (gets DN 1001)
python -m examples.run_cli
phone# set server 127.0.0.1
phone# set mac AABBCCDDEE02
phone# set model 7970
phone# connect
phone# show config

# On phone A — dial phone B's DN from show config
phone# call 1001
# On phone B — answer when it rings
phone# send softkey Answer
phone# show calls

Auto-answer — on the CLI softphone:

phone# set auto_answer true
phone# connect

Incoming calls are answered automatically via the Answer softkey. Persisted in examples/cli.config like other set options.

On the simulator, auto-answer can be enabled server-side (useful for headless tests):

python -m examples.run_simulator -v --auto-answer AABBCCDDEE02
python -m examples.run_simulator -v --auto-answer-all

RTP media is negotiated after connect (OpenReceiveChannel / StartMediaTransmission); audio path is basic but call state should reach Connected.

This is not a full CUCM replacement — no call routing or AXL — but it is useful for lab automation and client development without a Windows CallManager VM.

Lab troubleshooting (7912 + simulator)

Use two channels in parallel:

Tool Role
examples/run_simulator SCCP/TFTP (register, dial tone, call state)
python -m utils.phone_remote HTTP CGI — press softkeys / digits on the physical LCD
python -m utils.phone_web_probe Diagnose phone HTTP (GET / vs /CGI/*)
set PHONE_IP=10.102.10.209
set PHONE_USER=Administrator
set PHONE_PASS=your_phone_web_password

python -m utils.phone_web_probe --ip %PHONE_IP% --user %PHONE_USER% --password %PHONE_PASS%
python -m utils.phone_remote screenshot -o screen.png
python -m utils.phone_remote newcall          # Soft2 by default (PHONE_NEW_CALL_SOFTKEY)
python -m utils.phone_remote keys 1001
python -m utils.phone_remote interactive

Web UI — browser-based remote control with live LCD refresh when /CGI/Screenshot works:

python -m examples.run_phone_web
# open http://127.0.0.1:8765/
# or: pyskinny-phone-web --host 0.0.0.0   # LAN access (lab only)

Enter the phone IP and web credentials, click Probe — softkeys/keypad enable when /CGI/Execute works even if screenshots are unavailable.

Console web UI — control the running curses softphone in your browser (synthetic LCD, same SCCP session):

pyskinny-console --config --web-port 8766
pyskinny-cli --config --web-port 8766
pyskinny-macro --config --macro-file examples/ivr.macro --web-port 8766
# open http://127.0.0.1:8766/

For pyskinny-cli, the web UI starts after you connect (or on startup if auto_connect is enabled). Softkeys and keypad call SCCPClient directly (not HTTP to a hardware phone). Use --web-host 0.0.0.0 only on trusted lab networks.

7912 + phone_remote: The embedded web server is not a normal home page on port 80. curl http://<phone>/ often yields empty reply even when CGI push works. Use phone_web_probe to test /CGI/Screenshot and /CGI/Execute. On CUCM, <webAccess>0</webAccess> in SEP*.cnf.xml is normal when the admin “Web Access” checkbox is checked (0 = enabled, 1 = disabled). Clear <proxyServerURL></proxyServerURL> in the sim TFTP config — a proxy URL breaks local HTTP/CGI on 7912-class phones (the simulator patches this automatically). XML push auth goes to the simulator’s authenticate.asp, which must return plain AUTHORIZED (not XML).

On register, the simulator log should show legacy for a 7912 (type=0x7537). New Call should log outbound dial ... tone=33 (0x21) legacy=True matching CUCM (cm_cap.pcapng: SetRinger → SetSpeakerMode → SetLamp → CallState OffHook → SelectSoftKeys → DisplayPromptStatus → ActivateCallPlane → StartTone tone 33 dir 0).

Wireshark on the sim host:

"c:\Program Files\Wireshark\tshark.exe" -i 2 -f "host <phone-ip> and tcp port 2000"

Roadmap / TODO / Ideas

Live CM labs (cm2, cm31, cm33, cm41, cm43): integration tests cover registration, connect/hangup, hold/resume, blind transfer, consulted transfer. See docs/lab-softkey-hold.md and docs/lab-cm2-buttons.md.

Done

  • CallManager simulator, admin UI, TFTP relay, capture regression tests
  • Console / CLI / macro softphones; web UI; auto-reconnect after CM Reset
  • Hold/resume, blind & consulted transfer, conference (sim + client + live labs)
  • CM2 button phones: Stimulus hold (3), transfer (4), SetLamp call tracking, synthetic cm2-N refs
  • GitHub Actions CI; pip install -e ".[dev]"

Later

  • SIP phone support
  • Additional Skinny codecs (G.729 TX, wideband) beyond G.711 monitor

Use responsibly

These endpoints are old and permissive. Keep usage to trusted lab/LANs and authorized systems. Don’t enable phone web access on exposed networks.


License

MIT

About

CallManager Skinny / SCCP Tools and Clients

Resources

Stars

Watchers

Forks

Releases

Packages

Contributors

Languages