pycrestron-cip
pycrestron-cip is a typed asyncio client for CIP (Crestron-over-IP), the protocol that touch panels and
XPanel use to talk to a Crestron control processor. Your code connects as a panel IP ID, sees what that panel
would see (digital, analog and serial joins) 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.
Status: alpha. Tested against a Crestron 3-Series processor. The API may still change.
Not affiliated with, endorsed by or supported by Crestron Electronics, Inc. "Crestron" is a trademark of Crestron Electronics, Inc.
Features
- Pure
asyncio, no runtime dependencies, fully typed (py.typed) - Registration, update request / end-of-query sync, heartbeats in both directions
- Correct TCP framing: frames split across reads, several frames per read, several joins per sub-packet (processors pack their initial dump; older clients lose most of it)
- Liveness timeout: a silent, half-open connection is noticed and reconnected
- Automatic reconnect with backoff, and a long retry interval for an undefined IP ID
- Input caches with callbacks; latched outputs are replayed after every reconnect
- Buttons:
press/releasewith auto-repeat, andpulse - Serial joins in UTF-8 and UTF-16, chunked long strings, short and extended forms, smart objects
- Optional TLS (port 41796) and username/password authentication
FakeProcessor: a CIP server for your own tests, built from real capturespycrestron-cip-probe: a read-only command that records everything a processor sends
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 open-source CIP clients¹ | |
|---|---|---|
| Reads every join in a packed sub-packet (processors send their initial dump this way) | ✅ digital and analog, verified on a 3-Series | none do both; 6 of 7 keep only the first digital join |
| 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 and replays latched outputs | ✅ both | backoff: 2 of 7 · replay: 2 of 7 |
| Frames split across TCP reads | ✅ | 2 of 6 TCP clients |
| Serial text: UTF-8, UTF-16 (unicode flag), long strings sent in chunks | ✅ all three | UTF-16: 0 of 7 · chunks: 0 of 7 |
| A non-ASCII byte cannot break the receive path | ✅ | 3 of the 6 that decode serials |
| Smart-object joins, with the object ID | ✅ inbound | 0 of 7 complete |
| 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.
Why it matters
- Correct state from the first second. After connecting, 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.
- Connections that heal themselves. A client that never notices a half-open socket can sit "connected" while nothing arrives. This one sends and answers heartbeats, times out when the processor goes quiet, reconnects with backoff and restores its outputs.
- Any text, any language. Room names and labels come through intact: UTF-8, UTF-16 and long chunked strings, with a safe fallback for stray bytes.
- Built to be depended on. Pure asyncio, fully typed, no runtime dependencies, so it drops into Home Assistant
and other asyncio apps.
FakeProcessorandpycrestron-cip-probelet you test your integration without, and then against, real hardware.
Honest limits: sending to smart objects is not supported yet; TLS and username/password authentication are implemented but not yet verified on hardware (captures welcome); the WebSocket/CH5 transport used by some 4-Series web panels is not covered.
Requirements
- Python 3.13 or newer
- A Crestron processor reachable on TCP 41794 (or 41796 for TLS)
- A panel IP ID defined in the processor's program that nothing else is using (see below)
Installation
pip install pycrestron-cip
For local development:
git clone https://github.com/Antonio112009/pycrestron-cip.git
cd pycrestron-cip
pip install -e ".[dev]"
Quick Start
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())
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(). - Two clients on one IP ID share one panel. On paged panels that means one shared page: one client's navigation changes what the other sees. Give automation its own IP ID where you can.
- 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
Stop anything else that uses the same IP ID first. Please do not publish probe logs from a real installation: they contain room names, addresses and whatever the panel shows.
Protocol
docs/protocol.md describes the wire format as implemented, what was verified on hardware, other implementations and the open questions.
Contributing
See CONTRIBUTING.md. Captures from other processors (4-Series, TLS, authentication) are especially welcome.
License
Release files for pycrestron-cip 0.1.1
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.1.tar.gz | 31.0 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| pycrestron_cip-0.1.1-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 53.9 kB
Release files / pycrestron_cip-0.1.1.tar.gz
| Download URL | pycrestron_cip-0.1.1.tar.gz |
|---|---|
| Size | 31.0 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
16c5d05bbc4f2ab70fde9c5daba60ccd1adbf54f3a4defbd2954f1269b143e0c
|
|
BLAKE2b-256 checksum How to use checksums |
c0eb07b0e9e3735e813ea7be32609dd55b07b0d4abc24ee11915bb6607b2e593
|
| 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.1-py3-none-any.whl
| Download URL | pycrestron_cip-0.1.1-py3-none-any.whl |
|---|---|
| Size | 22.9 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
8a9003b9bc1ac8398fbd53b8be2cbd6483279696aa676437902423f6944b223a
|
|
BLAKE2b-256 checksum How to use checksums |
c4eb3b2c044ed83e3f334abae8dbe7a815a3b4af5f48829a7dd83bb0089a1607
|
| 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