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 14-byte record
Section titled “The 14-byte record”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.)
| Offset | Field | Type | Notes |
|---|---|---|---|
| 0 | prog_type | byte | ProgramType enum |
| 1-2 | cond | BE u16 | Primary condition (opaque this pass) |
| 3-4 | cond2 | BE u16 | Secondary condition (opaque this pass) |
| 5 | cmd | byte | Command enum |
| 6 | par | byte | Command parameter (level, mode, …) |
| 7-8 | pr2 | BE u16 | Secondary parameter (usually object number) |
| 9-10 | month, day | bytes | Swapped to day, month on disk for EVENT type — see below |
| 11 | days | byte | Days bitmask (Mon=0x02 … Sun=0x80) |
| 12 | hour | byte | 0-23 for absolute time; offset for sunrise/sunset-relative |
| 13 | minute | byte | 0-59 |
Remark variant
Section titled “Remark variant”When prog_type == REMARK (4) the bytes at offsets 1-4 hold a single
32-bit BE RemarkID instead of cond + cond2:
| Offset | Field | Type |
|---|---|---|
| 0 | prog_type (= 4) | byte |
| 1-4 | remark_id | BE 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.
ProgramType (byte 0)
Section titled “ProgramType (byte 0)”| Value | Name | Meaning |
|---|---|---|
| 0 | FREE | Unused slot (all 14 bytes zero) |
| 1 | TIMED | Fires at a specific time of day on selected weekdays |
| 2 | EVENT | Fires when a panel event occurs (zone open, X-10, …) |
| 3 | YEARLY | Fires on a specific calendar date each year |
| 4 | REMARK | Stores a RemarkID + free-text association |
| 5 | WHEN | Connector — string multiple records into one line (RE-pending) |
| 6 | AT | Connector (RE-pending) |
| 7 | EVERY | Connector (RE-pending) |
| 8 | AND | Connector (RE-pending) |
| 9 | OR | Connector (RE-pending) |
| 10 | THEN | Connector (RE-pending) |
Source: enuProgramType.cs.
ProgramCond (high bits of cond / cond2)
Section titled “ProgramCond (high bits of cond / cond2)”| Value | Name | Family |
|---|---|---|
| 0 | OTHER | catch-all / miscellaneous |
| 4 | ZONE | zone-state condition |
| 8 | CTRL | control-unit condition |
| 12 | TIME | time-clock condition |
| 16 | SEC | security-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.
Days (byte 11)
Section titled “Days (byte 11)”Bitmask with Monday as the low-order valid bit — not bit 0:
| Bit | Name |
|---|---|
0x02 | Monday |
0x04 | Tuesday |
0x08 | Wednesday |
0x10 | Thursday |
0x20 | Friday |
0x40 | Saturday |
0x80 | Sunday |
Source: enuDays.cs. 0x00 = no day selected.
Worked example
Section titled “Worked example”Take this slot from our live fixture (slot 22):
01 8d 09 9b 09 44 03 01 00 08 0c 3e 07 0fDecoded as a file record:
| Offset | Bytes | Field | Value |
|---|---|---|---|
| 0 | 01 | prog_type | TIMED (1) |
| 1-2 | 8d 09 | cond | 0x8d09 |
| 3-4 | 9b 09 | cond2 | 0x9b09 |
| 5 | 44 | cmd | 0x44 (raw — opcode is one of the many enuUnitCommand values) |
| 6 | 03 | par | 3 |
| 7-8 | 01 00 | pr2 | 256 |
| 9 | 08 | month (TIMED → no swap) | 8 |
| 10 | 0c | day | 12 |
| 11 | 3e | days | Mon|Tue|Wed|Thu|Fri (weekdays) |
| 12 | 07 | hour | 07 |
| 13 | 0f | minute | 15 |
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.”
Quirk: the EVENT Mon/Day swap
Section titled “Quirk: the EVENT Mon/Day swap”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 == 5assert p_wire.month == 12 and p_wire.day == 5assert p_disk == p_wire # same semantic ProgramThe 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.
Wire vs on-disk parity
Section titled “Wire vs on-disk parity”.pca file (slot in table) | clsOLMsgProgramData (wire) | |
|---|---|---|
| Total bytes | 14 (slot index implicit) | 16 (prefix + body) |
| Prefix | — | [program_number_hi, program_number_lo] BE u16 |
| Body | 14 bytes as documented above | 14 bytes — never swaps Mon/Day |
| Identified by | Position in 1500-slot block | clsOLMsgProgramData.ProgramNumber (Data[1-2]) |
The omni_pca.programs module distinguishes:
Program.from_wire_bytes(body) # for OmniLink message repliesProgram.from_file_record(body) # for .pca table slotswith matching encode_wire_bytes() / encode_file_record() round-trip
methods.
What we don’t yet know
Section titled “What we don’t yet know”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/cond2internal bit split. The high bits encode theProgramCondfamily (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 / THENProgType 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;
RemarkIDdecoded fine, but we can’t resolve it to a string today. DoubleProgramConditionalcapability 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 > 23orminute > 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.
Python usage
Section titled “Python usage”from omni_pca.pca_file import parse_pca_file, KEY_EXPORTfrom 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 theEvtu16 view.enuProgramType.cs,enuProgramCond.cs,enuDays.cs— the byte enums mirrored inomni_pca.programs.clsHAC.cs:180-549— wire-formatclsOLMsgProgramData(v1) and:1180-1330—clsOL2MsgProgramData(v2). Both prepend a 2-byte BEProgramNumberto the 14-byte body..pcafile format — where the 21,000-byte Programs block sits in the body walk.- Library API →
Program— public surface forProgram.from_wire_bytes/from_file_recordetc.