Skip to main content

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.

CI PyPI Python Zero dependencies Typed Ruff License: MIT

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 asyncio

No runtime dependencies, fully typed (py.typed). Drops straight into Home Assistant and other asyncio apps.

🎯 Complete state

Reads every join in a packed sub-packet, so the processor's initial dump arrives intact.

🔁 Self-healing

Heartbeats both ways, a liveness timeout for half-open sockets, reconnect with backoff, outputs replayed.

🔘 Real buttons

press / release with auto-repeat like a finger on the glass, and pulse for a tap.

🌍 Any text

Serial joins in UTF-8 and UTF-16, long strings in chunks, short and extended forms.

🧩 Smart objects

Incoming smart-object joins, each tagged with the object ID it came from.

🧱 Correct framing

Frames split across TCP reads, several frames per read, several joins per sub-packet — all handled.

🔒 TLS & auth

Optional TLS on port 41796 and username/password authentication.

🧪 Test kit

FakeProcessor for your own tests, read-only pycrestron-cip-probe for real hardware.

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 on
FakeProcessor 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 until release().
  • 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

📘 Protocol

The wire format as implemented, what was verified on hardware, other implementations and open questions.

🤝 Contributing

Captures from other processors (4-Series, TLS, authentication) are especially welcome.

📄 License

MIT 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)

Source distribution for pycrestron-cip 0.1.2
File Size Uploaded
pycrestron_cip-0.1.2.tar.gz 31.7 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for pycrestron-cip 0.1.2
File Interpreter ABI Platform
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 log

Release 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

Release history Release notifications | RSS feed

This release

0.1.2 This release

2 release files

0.1.1

2 release files

0.1.0

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page