Skip to content

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.

  • Python 3.14+ and uv installed.
  • Either:
Terminal window
mkdir omni-hello && cd omni-hello
uv init --no-readme --python '>=3.14'
uv add omni-pca

You’ll get a pyproject.toml with omni-pca listed and a hello.py stub.

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 4369
KEY = 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())
Terminal window
uv run python hello.py

Against 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_MOTION

If 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.

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.

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.

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.