Write your first omni_pca script
Twenty lines of Python that connect to an Omni panel, fetch its model and firmware, walk every named zone, and stream typed events as they arrive. Run against the mock if you don’t have a panel yet, or a real one once you have its ControllerKey.
What you need
Section titled “What you need”- Python 3.14+ and
uvinstalled. - Either:
- The dev stack running (mock at
127.0.0.1:14369with key000102030405060708090a0b0c0d0e0f), or - A real panel reachable on TCP/4369 with its ControllerKey extracted via the .pca decryption tutorial.
- The dev stack running (mock at
Step 1 — bootstrap a script
Section titled “Step 1 — bootstrap a script”mkdir omni-hello && cd omni-hellouv init --no-readme --python '>=3.14'uv add omni-pcaYou’ll get a pyproject.toml with omni-pca listed and a hello.py
stub.
Step 2 — replace hello.py
Section titled “Step 2 — replace hello.py”"""First contact with an Omni-Link II panel."""
from __future__ import annotations
import asyncio
from omni_pca import OmniClient
# Pick one — mock OR real panel.HOST = "127.0.0.1" # or "192.168.1.9"PORT = 14369 # or 4369KEY = bytes.fromhex("000102030405060708090a0b0c0d0e0f")# KEY = bytes.fromhex("YOUR_REAL_CONTROLLER_KEY_HERE")
async def main() -> None: async with OmniClient(host=HOST, port=PORT, controller_key=KEY) as panel: info = await panel.get_system_information() print(f"Connected: {info.model_name} firmware {info.firmware_version}")
zones = await panel.list_zone_names() print(f"\n{len(zones)} named zones:") for index, name in sorted(zones.items()): print(f" {index:>3}: {name}")
if __name__ == "__main__": asyncio.run(main())Step 3 — run it
Section titled “Step 3 — run it”uv run python hello.pyAgainst the dev-stack mock you’ll see something like:
Connected: Omni Pro II firmware 2.12r1
5 named zones: 1: FRONT_DOOR 2: GARAGE_ENTRY 3: BACK_DOOR 10: LIVING_MOTION 11: HALL_MOTIONIf you see this, the four-step secure-session handshake completed: the
client opened a TCP connection, sent ClientRequestNewSession, parsed
the controller’s ControllerAckNewSession (5-byte SessionID), derived
the session key with the XOR mix,
sent ClientRequestSecureSession AES-encrypted with per-block
whitening,
got back ControllerAckSecureSession, then issued
RequestSystemInformation (opcode 22) and walked
RequestProperties (opcode 32) for every zone.
Step 4 — react to push events
Section titled “Step 4 — react to push events”Append to hello.py:
async def watch_events() -> None: async with OmniClient(host=HOST, port=PORT, controller_key=KEY) as panel: print("Listening for unsolicited events. Trigger something on the panel...") async for event in panel.events(): print(f" {type(event).__name__}: {event}")
if __name__ == "__main__": asyncio.run(watch_events())Run it again. Now go open the dev-stack HA UI and toggle a light, or arm an area, or bypass a zone. You’ll see typed events stream in as the mock pushes them:
Listening for unsolicited events. Trigger something on the panel... UnitStateChanged: UnitStateChanged(unit_index=1, new_state=1, ...) ArmingChanged: ArmingChanged(area_index=1, new_mode=3, user_index=1, ...) ZoneStateChanged: ZoneStateChanged(zone_index=1, ...)Each subclass of SystemEvent has its own typed fields. The
library API reference lists all
26 subclasses.
Step 5 — issue a command
Section titled “Step 5 — issue a command”Replace watch_events with:
async def turn_on_living_lamp() -> None: async with OmniClient(host=HOST, port=PORT, controller_key=KEY) as panel: await panel.turn_unit_on(1) # mock: index 1 = LIVING_LAMP await panel.set_unit_level(1, 60) # 60% brightness statuses = await panel.get_extended_status_for("UNIT", 1) print(statuses)That’s a Command packet (opcode 20) with command_byte=UNIT_LEVEL,
parameter1=60, parameter2=1 — followed by an extended-status read so
you can confirm the change took. Against the mock the unit’s state
byte will be 160 (= 100 + percent), and the brightness conversion in
the HA light platform turns that back into HA’s 0-255 scale.
What just happened
Section titled “What just happened”You exercised three of the library’s core surfaces: read (system info,
properties walk), push (typed event stream), and write (command
dispatch). All of them go through the same OmniClient async context
manager, which owns the OmniConnection underneath; the connection
handles framing, CRC, AES with the per-block whitening, and sequence-
number tracking.
The full surface is on the library API reference. What we covered here is maybe 5% of it; setpoints, area arming, button macros, program execution, audio control, raw command escape hatches all work the same way.
Where next
Section titled “Where next”- How-to: bypass a zone — a focused recipe.
- How-to: send a panel display message — for “the laundry is done”-style notifications.
- Reference: full Omni-Link II protocol spec — if you want to read raw packets, not just call client methods.