Skip to content

Program record format

A “program” in Omni Pro II vocabulary is one line of the panel’s built-in automation engine: roughly “if X happens at time Y, do Z.” The panel stores up to 1500 programs in a fixed-size table — in a .pca file they live in a 21,000-byte block (1500 × 14 bytes); on the wire they are exchanged one at a time via clsOLMsgProgramData / clsOL2MsgProgramData. PC Access exposes a visual editor for them; the HA integration currently surfaces them as raw decoded fields only.

The user-facing model (every program line has a WHEN, an optional &IF condition, and a THEN) is documented in the Owner’s Manual under “Programming” and the Installation Manual under “SETUP MISC → Programs”.

The Omni Pro II has the DoubleProgramConditional feature flag set so every record is 14 bytes with two condition slots. (Models without the flag would use 12 bytes with one cond — we don’t yet support those.)

OffsetFieldTypeNotes
0prog_typebyteProgramType enum
1-2condBE u16Primary condition (opaque this pass)
3-4cond2BE u16Secondary condition (opaque this pass)
5cmdbyteCommand enum
6parbyteCommand parameter (level, mode, …)
7-8pr2BE u16Secondary parameter (usually object number)
9-10month, daybytesSwapped to day, month on disk for EVENT type — see below
11daysbyteDays bitmask (Mon=0x02 … Sun=0x80)
12hourbyte0-23 for absolute time; offset for sunrise/sunset-relative
13minutebyte0-59

When prog_type == REMARK (4) the bytes at offsets 1-4 hold a single 32-bit BE RemarkID instead of cond + cond2:

OffsetFieldType
0prog_type (= 4)byte
1-4remark_idBE u32
5-13(unused, typically zero)bytes

The lookup from remark_id to the user-visible remark string lives in a separate table on disk that we have not yet reverse-engineered.

ValueNameMeaning
0FREEUnused slot (all 14 bytes zero)
1TIMEDFires at a specific time of day on selected weekdays
2EVENTFires when a panel event occurs (zone open, X-10, …)
3YEARLYFires on a specific calendar date each year
4REMARKStores a RemarkID + free-text association
5WHENConnector — string multiple records into one line (RE-pending)
6ATConnector (RE-pending)
7EVERYConnector (RE-pending)
8ANDConnector (RE-pending)
9ORConnector (RE-pending)
10THENConnector (RE-pending)

Source: enuProgramType.cs.

ValueNameFamily
0OTHERcatch-all / miscellaneous
4ZONEzone-state condition
8CTRLcontrol-unit condition
12TIMEtime-clock condition
16SECsecurity-mode condition

Source: enuProgramCond.cs. The internal bit-split of cond (selector vs operand within the 16-bit field) is not yet decoded — see “What we don’t yet know” below.

Bitmask with Monday as the low-order valid bit — not bit 0:

BitName
0x02Monday
0x04Tuesday
0x08Wednesday
0x10Thursday
0x20Friday
0x40Saturday
0x80Sunday

Source: enuDays.cs. 0x00 = no day selected.

Take this slot from our live fixture (slot 22):

01 8d 09 9b 09 44 03 01 00 08 0c 3e 07 0f

Decoded as a file record:

OffsetBytesFieldValue
001prog_typeTIMED (1)
1-28d 09cond0x8d09
3-49b 09cond20x9b09
544cmd0x44 (raw — opcode is one of the many enuUnitCommand values)
603par3
7-801 00pr2256
908month (TIMED → no swap)8
100cday12
113edaysMon|Tue|Wed|Thu|Fri (weekdays)
1207hour07
130fminute15

So: “TIMED program firing at 07:15 on weekdays, doing command 0x44 with par=3 and pr2=256, gated by the condition pair 0x8d09 / 0x9b09.”

For EVENT-typed programs on disk only, bytes 9 and 10 are stored in the order [day, month] instead of [month, day]. The on-the-wire clsOLMsgProgramData reply does not apply this swap — only clsProgram.Read / Write (the file-IO methods) do, at clsProgram.cs:471-484 and :506-515.

The Python decoder hides this:

from omni_pca.programs import Program
# These two byte strings are the same EVENT program — on disk vs on wire.
disk_bytes = bytes.fromhex("02 0c04 0000 01 01 0000 05 0c 00 07 0f".replace(" ", ""))
wire_bytes = bytes.fromhex("02 0c04 0000 01 01 0000 0c 05 00 07 0f".replace(" ", ""))
p_disk = Program.from_file_record(disk_bytes)
p_wire = Program.from_wire_bytes(wire_bytes)
assert p_disk.month == 12 and p_disk.day == 5
assert p_wire.month == 12 and p_wire.day == 5
assert p_disk == p_wire # same semantic Program

The encoder re-applies the swap on the way out, so file/wire round-trip stability is preserved.

Also for EVENT: bytes 9/10 are an event identifier, not a date

Section titled “Also for EVENT: bytes 9/10 are an event identifier, not a date”

clsProgram.Evt (line 152) defines a u16 view: Evt = (Mon << 8) | Day. For EVENT-typed programs the calendar-month/calendar-day interpretation is a misnomer — those two bytes encode a 16-bit event identifier (which zone triggered, which X-10 code received, etc.). The dataclass exposes event_id as a convenience:

p = Program.from_wire_bytes(wire_bytes)
if p.prog_type == ProgramType.EVENT:
print(f"event_id = 0x{p.event_id:04x}")

The selector/operand decoding inside event_id is part of the same “what we don’t know” set as cond semantics.

.pca file (slot in table)clsOLMsgProgramData (wire)
Total bytes14 (slot index implicit)16 (prefix + body)
Prefix[program_number_hi, program_number_lo] BE u16
Body14 bytes as documented above14 bytes — never swaps Mon/Day
Identified byPosition in 1500-slot blockclsOLMsgProgramData.ProgramNumber (Data[1-2])

The omni_pca.programs module distinguishes:

Program.from_wire_bytes(body) # for OmniLink message replies
Program.from_file_record(body) # for .pca table slots

with matching encode_wire_bytes() / encode_file_record() round-trip methods.

This page covers the byte-level mechanics. The semantic decoding of cond / cond2 — what zone number, security mode, or time clock a specific 16-bit value refers to — is a separate reverse-engineering pass. So is the multi-record clausal encoding hinted at by the WHEN/AT/EVERY/AND/OR/THEN connector values in ProgramType. Concretely:

  • cond / cond2 internal bit split. The high bits encode the ProgramCond family (Zone / Ctrl / Time / Sec); we don’t yet know where the selector index (zone number, etc.) and the operand (“not ready”, “Day mode”, …) live in the low bits. None of the 330 defined programs in our fixture is enough to triangulate this — we’d need to author known programs in PC Access and diff the exported bytes.
  • Multi-record clausal encoding. No program in our live fixture uses the WHEN / AT / EVERY / AND / OR / THEN ProgType values — so we can’t yet say whether they reference adjacent slots, use extra bytes within a single slot, or live in some separate clause table.
  • RemarkID → RemarkText lookup. The remark-text table on disk has not been located; RemarkID decoded fine, but we can’t resolve it to a string today.
  • DoubleProgramConditional capability flag. We hardcode 14-byte records for the Omni Pro II. Other models may use the 12-byte form. Locating where the panel advertises the flag (and which non-OPII models clear it) is its own follow-up.
  • TIMED time-of-day encoding. Some TIMED programs in the live fixture have hour > 23 or minute > 59, suggesting bytes 12/13 also encode sunrise/sunset-relative offsets (Owner’s Manual: ±0-120 minutes). The flag distinguishing absolute time from relative offset has not been isolated.
from omni_pca.pca_file import parse_pca_file, KEY_EXPORT
from omni_pca.programs import ProgramType, iter_defined
acct = parse_pca_file("/path/to/Account.pca", key=KEY_EXPORT)
for p in iter_defined(acct.programs):
t = ProgramType(p.prog_type).name
print(f"slot {p.slot:4} {t:6} cmd=0x{p.cmd:02x} "
f"{p.hour:02d}:{p.minute:02d} days=0x{p.days:02x}")

acct.programs is a tuple[Program, ...] of length 1500; iter_defined filters to in-use slots (matches the panel’s “non-FREE” definition).

References:

  • clsProgram.cs — field accessors, the Read/Write swap for Event, ToByteArray / FromByteArray, and the Evt u16 view.
  • enuProgramType.cs, enuProgramCond.cs, enuDays.cs — the byte enums mirrored in omni_pca.programs.
  • clsHAC.cs:180-549 — wire-format clsOLMsgProgramData (v1) and :1180-1330clsOL2MsgProgramData (v2). Both prepend a 2-byte BE ProgramNumber to the 14-byte body.
  • .pca file format — where the 21,000-byte Programs block sits in the body walk.
  • Library API → Program — public surface for Program.from_wire_bytes / from_file_record etc.