pycrestron-cip — asyncio client for Crestron-over-IP
Your code becomes a touch panel. See every digital, analog and serial join a Crestron processor sends,
press any button — without touching the processor's program.
Quick start · Highlights · Why · API · Testing · Probe · Protocol
CIP (Crestron-over-IP) is the protocol that touch panels and XPanel use to talk to a Crestron control processor.
pycrestron-cip connects as a panel IP ID, sees what that panel would see and can press its buttons.
It needs nothing from the processor's program: no SIMPL changes, no extra modules. If a panel IP ID is defined, you can connect to it. That makes it a good base for Home Assistant integrations and other home automation.
Highlights
⚡ Pure asyncioNo runtime dependencies, fully typed ( |
🎯 Complete stateReads every join in a packed sub-packet, so the processor's initial dump arrives intact. |
🔁 Self-healingHeartbeats both ways, a liveness timeout for half-open sockets, reconnect with backoff, outputs replayed. |
🔘 Real buttons
|
🌍 Any textSerial joins in UTF-8 and UTF-16, long strings in chunks, short and extended forms. |
🧩 Smart objectsIncoming smart-object joins, each tagged with the object ID it came from. |
🧱 Correct framingFrames split across TCP reads, several frames per read, several joins per sub-packet — all handled. |
🔒 TLS & authOptional TLS on port 41796 and username/password authentication. |
🧪 Test kit
|
Quick start
pip install pycrestron-cip
import asyncio
from pycrestron_cip import CipClient, JoinUpdate
def on_join(update: JoinUpdate) -> None:
print(f"{update.type}{update.join} = {update.value!r}")
async def main() -> None:
async with CipClient("192.168.1.10", 0x03) as panel:
panel.subscribe(on_join) # everything the processor sends
await panel.pulse(101) # tap button d101
print(panel.get_analog(361)) # last value of a361
panel.set_serial(10, "hello") # latched serial output
await asyncio.sleep(30)
asyncio.run(main())
You need: Python 3.13 or newer · a processor reachable on TCP 41794 (41796 for TLS) · a panel IP ID defined in the processor's program that nothing else is using (see below).
Local development
git clone https://github.com/Antonio112009/pycrestron-cip.git
cd pycrestron-cip
pip install -e ".[dev]"
Why pycrestron-cip
Most open-source CIP clients stop at "it connects". We read the source of every panel-side CIP client we could find, ran their parsers against a fake processor, and built this library to close the gaps. Then we verified it on a real processor.
| pycrestron-cip | Other clients¹ | |
|---|---|---|
|
🎯 Correct state from the first second A processor packs several joins into one sub-packet of its initial dump. A client that reads only the first one starts with most values missing, and a paged panel may not resend them until they change. That is how a light ends up shown as off while it is on. |
||
| Reads every join in a packed sub-packet | ✅ digital and analog, verified on a 3-Series |
none do both; 6 of 7 keep only the first digital join |
| Frames split across TCP reads | ✅ | 2 of 6 TCP clients |
| Smart-object joins, with the object ID | ✅ inbound | 0 of 7 complete |
|
🔁 Connections that heal themselves A client that never notices a half-open socket can sit "connected" while nothing arrives. |
||
| Answers the processor's heartbeat requests | ✅ | 1 of 7 |
| Notices a silent, half-open connection by itself | ✅ liveness timeout | 0 of 7 |
| Reconnects with backoff | ✅ | 2 of 7 |
| Replays latched outputs after a reconnect | ✅ | 2 of 7 |
|
🌍 Any text, any language Room names and labels come through intact, with a safe fallback for stray bytes. |
||
| Serial text in UTF-16 (unicode flag) | ✅ and UTF-8 | 0 of 7 |
| Long strings sent in chunks | ✅ | 0 of 7 |
| A non-ASCII byte cannot break the receive path | ✅ | 3 of the 6 that decode serials |
🧪 Built to be depended onFakeProcessor and pycrestron-cip-probe let you test your integration without, and then against, real hardware.
|
||
| A fake processor you can use in your own tests | ✅ pycrestron_cip.testing |
0 of 7 ship one |
| Automated tests | ✅ 41 tests, 94 % coverage, CI on Python 3.13 and 3.14 |
3 of 7 |
¹ Seven XPanel-style CIP clients in Python, Swift, JavaScript and Node, each reviewed at its latest commit in September 2026. Projects that emulate a processor, proxy TLS on the processor, or use a different protocol are not counted. Other projects were tested against a fake processor, not real hardware.
API
| Call | What it does |
|---|---|
await connect() / async with CipClient(...) |
Connect, register, wait for the initial sync. Raises IpidNotDefinedError, AuthError, CipTimeoutError or CipConnectionError |
await close() |
Say goodbye to the processor and stop all tasks |
get_digital/analog/serial(join) |
Last value the processor sent (the default when never sent) |
has_input(join_type, join) |
Whether the processor has sent this join at all |
subscribe(cb, join_type=None, join=None) |
Callback for incoming joins; returns an unsubscribe function |
subscribe_state(cb) |
Callback for connection state changes (ConnectionState) |
set_digital/analog/serial(join, value) |
Latched outputs, replayed after every reconnect |
press(join) / release(join) / await pulse(join) |
Buttons (held buttons auto-repeat) |
request_update() |
Ask the processor to send its values again |
available, state, await wait_ready() |
Connection status |
snapshot(), stats, last_error |
Diagnostics (no credentials) |
Smart-object joins: pass smart_object= to the getters; JoinUpdate.smart_object tells you where an update
came from.
Options
| Option | Default | Meaning |
|---|---|---|
port |
41794 (41796 with TLS) | TCP port |
ssl |
None |
True for TLS without certificate checks, or an ssl.SSLContext |
username, password |
None |
Credentials, if the processor requires authentication |
connect_timeout |
10 s | Connect, register and sync must finish within this time |
heartbeat_interval |
15 s | How often we send a heartbeat (processors answer, they do not ask) |
liveness_timeout |
35 s | Nothing received for this long means the connection is dead |
reconnect |
True |
Reconnect by itself after a successful first connect |
backoff_min, backoff_max |
1 s, 60 s | Reconnect backoff |
button_repeat |
0.5 s | Repeat interval for held buttons |
Things to know about processors
- The processor sends a panel only changes relative to what it believes the panel already shows. After a (re)connect, the initial dump contains non-default values only, and a "paged" panel may not resend a value until it changes.
- Button-style digitals are released by the processor unless repeated within about 0.5 s.
press()repeats for you untilrelease(). - Directly setting an analog that the program drives (for example a dimmer level) often does nothing: the program expects the panel's raise/lower buttons.
Testing your code
FakeProcessor is a small CIP server that behaves like a real processor as far as observed: it greets, registers
panels, answers heartbeats, packs its initial dump and ends it with end-of-query. It can also misbehave: wrong
IP ID, silence, dropped connections, split or coalesced frames.
from pycrestron_cip import CipClient
from pycrestron_cip.testing import FakeProcessor
async def test_scene_indicator():
async with FakeProcessor(ipids={0x03}) as proc:
proc.digital[350] = True
async with CipClient("127.0.0.1", 0x03, port=proc.port) as panel:
assert panel.get_digital(350)
await panel.pulse(101)
assert [u.join for u in proc.joins_received()] == [101, 101] # press, release
Probing a processor
pycrestron-cip-probe (or python -m pycrestron_cip.probe) connects as a panel, records everything the processor
sends and disconnects. It sends only what the protocol requires (no button presses, no join values):
pycrestron-cip-probe 192.168.1.10 0x03 --seconds 120 --log probe.log --json probe.json
Learn more
📘 ProtocolThe wire format as implemented, what was verified on hardware, other implementations and open questions. |
🤝 ContributingCaptures from other processors (4-Series, TLS, authentication) are especially welcome. |
📄 LicenseMIT licensed. |
Not affiliated with, endorsed by or supported by Crestron Electronics, Inc. “Crestron” is a trademark of Crestron Electronics, Inc.
Release files for pycrestron-cip 0.1.2
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| pycrestron_cip-0.1.2.tar.gz | 31.7 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| pycrestron_cip-0.1.2-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 55.3 kB
Release files / pycrestron_cip-0.1.2.tar.gz
| Download URL | pycrestron_cip-0.1.2.tar.gz |
|---|---|
| Size | 31.7 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
0778946a937a2f86d8bb0fbfd0cb729a5d69f6da9e3a16c31171e4dc8015bc2e
|
|
BLAKE2b-256 checksum How to use checksums |
f2190513960339a022a74a52acfade629066c8e15c273e60a943812bd9949830
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Sep 27, 2026.
Transparency logRelease files / pycrestron_cip-0.1.2-py3-none-any.whl
| Download URL | pycrestron_cip-0.1.2-py3-none-any.whl |
|---|---|
| Size | 23.6 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
4f802326a782e7c3b27d3b889acd341a2944f23adcfaddd1f87a919b94608ad7
|
|
BLAKE2b-256 checksum How to use checksums |
6d699ee026f058ddd5542931888c98938aa60148a3fd2f183c2921b6ce9b5541
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Sep 27, 2026.
Transparency log