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 | LE u16 | First inline AND-IF condition (zero = no condition) |
| 3-4 | cond2 | LE u16 | Second inline AND-IF condition (zero = no condition) |
| 5 | cmd | byte | Command enum |
| 6 | par | byte | Command parameter (level, mode, …) |
| 7-8 | pr2 | LE 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 text lives in a
separate Remarks table further down the .pca body. Layout
(clsPrograms.ReadRemarks, clsPrograms.cs:148-168):
[u32 LE _RemarksNextID][u32 LE count][ for each entry: [u32 LE remark_id] [u16 LE text_length][text_length bytes UTF-8]]To reach the Remarks table from end-of-Connection, the walker skips
nine fixed-shape Description blocks (one per object family, each
[u32 count] + count × 33 bytes); see
clsHAC.cs:8055-8079. The Python parser puts the resolved dict on
PcaAccount.remarks:
from omni_pca.pca_file import parse_pca_file, KEY_EXPORTfrom omni_pca.programs import ProgramType, iter_defined
acct = parse_pca_file("Account.pca", key=KEY_EXPORT)for p in iter_defined(acct.programs): if p.prog_type == ProgramType.REMARK: text = acct.remarks.get(p.remark_id, "<unresolved>") print(f"slot {p.slot}: remark {p.remark_id} = {text!r}")ProgramType (byte 0)
Section titled “ProgramType (byte 0)”The 11 values split into two encoding families. Which family a block uses depends on the panel firmware version — see Compact vs multi-record form below.
| Value | Name | Family | Meaning |
|---|---|---|---|
| 0 | FREE | compact | Unused slot (all 14 bytes zero) |
| 1 | TIMED | compact | Time-of-day trigger with inline action |
| 2 | EVENT | compact | Panel event (zone open, X-10, …) with inline action |
| 3 | YEARLY | compact | Yearly calendar trigger with inline action |
| 4 | REMARK | compact | RemarkID + free-text association (see Remark variant) |
| 5 | WHEN | multi-record | Event-trigger record (firmware ≥3.0.0) |
| 6 | AT | multi-record | Time-trigger record (firmware ≥3.0.0) |
| 7 | EVERY | multi-record | Recurring-trigger record (firmware ≥3.0.0) |
| 8 | AND | multi-record | AND-condition record (firmware ≥3.0.0) |
| 9 | OR | multi-record | OR-alternative separator (firmware ≥3.0.0) |
| 10 | THEN | multi-record | Action record (firmware ≥3.0.0) |
Source: enuProgramType.cs; the firmware gate is
Features.Add(MultiLinePrograms, 196608u) at clsCapOMNI_PRO_II.cs:290.
ConditionFamily (high bits of cond / cond2)
Section titled “ConditionFamily (high bits of cond / cond2)”| Value | Name | Family |
|---|---|---|
| 0 | OTHER | misc-conditional (DARK, AC_POWER_OFF, …) |
| 4 | ZONE | zone-state condition |
| 8 | CTRL | control-unit (light/output) condition |
| 12 | TIME | time-clock condition |
| 16 | SEC | security-mode condition (catch-all) |
Family is found by (cond >> 8) & 0xFC — bits 2-7 of the high byte.
Source: enuProgramCond.cs. See “cond / cond2 bit
split” below for the full per-family decode.
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 (LE for u16) |
|---|---|---|---|
| 0 | 01 | prog_type | TIMED (1) |
| 1-2 | 8d 09 | cond | 0x098d → CTRL family, Unit 397, OFF |
| 3-4 | 9b 09 | cond2 | 0x099b → CTRL family, Unit 411, OFF |
| 5 | 44 | cmd | 0x44 (raw — opcode is one of the many enuUnitCommand values) |
| 6 | 03 | par | 3 |
| 7-8 | 01 00 | pr2 | 0x0001 = 1 (object #1) |
| 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 against object #1, gated by Unit 397 OFF AND IF Unit 411 OFF.”
Note that the bytes at offsets 7-8 are 01 00 but the value is 1
(not 256): the low byte comes first. Same for cond (8d 09 →
0x098d, not 0x8d09) and cond2.
cond/cond2 bit split
Section titled “cond/cond2 bit split”The 16-bit cond (and cond2) field packs family + selector + operand.
The high byte’s bits 2-7 (i.e. (cond >> 8) & 0xFC) discriminate the family;
the bottom byte and low bits of the high byte carry the rest.
| Family | Match | Bit layout | Selector | Operand |
|---|---|---|---|---|
OTHER | (cond >> 8) & 0xFC == 0x00 | ....‥‥‥‥ ‥‥‥‥mmmm | bits 0-3 = MiscConditional | (no operand) |
ZONE | ... == 0x04 | 000001oo zzzzzzzz | bits 0-7 = zone # | bit 9 = 0=SECURE, 1=NOT_READY |
CTRL | ... == 0x08 | 000010oo uuuuuuuu (+ bit 8 = high u) | bits 0-8 = unit # | bit 9 = 0=OFF, 1=ON |
TIME | ... == 0x0C | 000011oo cccccccc | bits 0-7 = clock # | bit 9 = 0=DISABLED, 1=ENABLED |
SEC | anything else (incl. 0x10+) | xmmm aaaa ........ | bits 8-11 = area # (0 = any) | bits 12-14 = SecurityMode, bit 15 = arming-transition flag |
Worked examples:
cond = 0x0605 → ZONE, zone 5, NOT_READY ("Zone 5 NOT_READY")cond = 0x0A0F → CTRL, unit 15, ON ("Unit 15 ON")cond = 0x0E03 → TIME, clock 3, ENABLED ("Time clock 3 ENABLED")cond = 0x000B → OTHER, BATTERY_OKcond = 0x8100 → SEC, area 1, mode=OFF ("Area 1 OFF")cond = 0xB100 → SEC, area 1, ARMING AWAY (bit 15 + mode=3)Python usage:
from omni_pca.programs import Condition, ConditionFamily, Program, ProgramType
p = Program( prog_type=int(ProgramType.TIMED), cond=0x0605, # "Zone 5 NOT_READY" cond2=0xB100, # "Area 1 ARMING AWAY")c1 = p.condition()assert c1.family is ConditionFamily.ZONEassert c1.selector == 5 and c1.operand == 1assert c1.describe() == "Zone 5 NOT_READY"Condition.describe() does the rendering with index-based labels
("Zone 5", "Unit 12") — name lookups need the panel name tables and
are left to the caller. A future helper that takes a name map would be
the obvious next step for a UI editor.
A few details worth knowing:
OTHERignores high bits.cond=0x010Bandcond=0x000Bboth decode toBATTERY_OK— PC Access’s encoder sometimes leaves high bits set on Other-family conditions; the decoder masks them off.SECwith mode=Off + bit 15 is the plain “Area X is OFF” encoding, NOT an arming transition. The arming-transition rendering kicks in only when bit 15 and the mode bits 12-14 are non-zero (perclsText.cs:2263).- Area
0inSECis the “no specific area” / “any area” form — PC Access renders the area name as blank in this case.
Source: clsText.GetConditionalText at clsText.cs:2224-2273 (decode) and
frmAutomationEditCondition.cs:615-2550 (encode/UI). The MiscConditional
enum mirrors enuMiscConditional.cs.
TIMED programs: absolute time vs sunrise/sunset offset
Section titled “TIMED programs: absolute time vs sunrise/sunset offset”The hour byte at offset 12 is overloaded as a one-of-three
discriminator:
hour byte | Meaning |
|---|---|
| 0 – 23 | Absolute wall-clock time. minute is unsigned 0-59. |
| 25 | Sunrise-relative. minute is read as a signed byte. |
| 26 | Sunset-relative. minute is read as a signed byte. |
For the sunrise/sunset forms, the signed minute byte is the offset
in minutes: positive = after, negative = before, zero = at.
from omni_pca.programs import Program, ProgramType, TimeKind
p = Program(prog_type=int(ProgramType.TIMED), hour=26, minute=246, days=0x40)assert p.time_kind == TimeKind.SUNSETassert p.time_offset_minutes == -10 # 246 as sbyte == -10assert p.format_time() == "10 min before sunset"The decoder hides the sbyte trick behind a time_kind property and a
time_offset_minutes view; the original hour / minute bytes are
still on the dataclass for round-trip fidelity. format_time()
renders a friendly label so HA entity callers get
"30 min after sunrise" rather than (25, 30).
Source: frmPopUpEditTime.cs:186-217 (decode) and :241-263 (encode)
in the decompiled PC Access form. The Owner’s Manual (“±0-120 minutes”)
is a UI-level constraint; the wire encoding supports the full
sbyte range (-128..127).
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.
Compact vs multi-record form
Section titled “Compact vs multi-record form”The same user-visible “program block” (1 WHEN + 0-N AND-IFs + 1 THEN) has two on-disk encodings. PC Access picks compact whenever the block can fit, and multi-record otherwise.
Compact form (always available)
Section titled “Compact form (always available)”The block fits in one 14-byte record. prog_type is TIMED,
EVENT, or YEARLY depending on the trigger kind:
- The action goes inline in
cmd/par/pr2. - Up to two AND-IF conditions go inline in
condandcond2. - For
EVENT, the event identifier replaces the calendar month/day at bytes 9-10 (see the EVENTEvtu16 section).
This is how 100% of records in any panel running firmware before 3.0 are
encoded. It is also how PC Access encodes any block that fits the
constraints, even on firmware ≥3.0 — see the simplification rules
in frmAutomationEditBlock.cs:589 SimplifyLines.
Multi-record form (firmware ≥3.0.0 only)
Section titled “Multi-record form (firmware ≥3.0.0 only)”When the block cannot fit in compact form, PC Access emits one 14-byte record per “line”, consuming sequential slots in the program table:
| ProgType | Role | Notable fields |
|---|---|---|
WHEN (5) | Event trigger | event-id at bytes 9-10 BE (wire form, no swap); cmd/par/pr2 zero |
AT (6) | Single-occurrence time trigger | month/day/days/hour/minute at bytes 9-13 (same as compact TIMED) |
EVERY (7) | Recurring time trigger | interval at bytes 3-4 BE |
AND (8) | One AND-IF condition | family + operand at byte 1; instance at bytes 3-4 BE |
OR (9) | Separator between AND-groups | no payload — only the prog_type byte is set |
THEN (10) | Action | same cmd / par / pr2 (LE) layout as compact form |
Per-record byte tables for each of these are in the per-record byte layouts section below.
A 5-record block from our test fixture (one WHEN, three ANDs, one THEN, all in adjacent slots):
slot N+0: 05 00 00 00 00 00 00 00 00 04 01 00 00 00 WHEN <Zone 1 Secure>slot N+1: 08 00 00 00 01 00 00 00 00 00 00 00 00 00 AND IF NEVERslot N+2: 08 00 00 00 01 00 00 00 00 00 00 00 00 00 AND IF NEVERslot N+3: 08 00 00 00 01 00 00 00 00 00 00 00 00 00 AND IF NEVERslot N+4: 0a 00 00 00 00 01 00 01 00 00 00 00 00 00 THEN UNIT 1 ONThe firmware gate is Features.Add(MultiLinePrograms, 196608u) in
clsCapOMNI_PRO_II.cs:290 (the 24-bit value packs as
major*65536 + minor*256 + revision → 3.0.0).
When MultiLinePrograms is OFF (firmware before 3.0):
- PC Access’s
Ortoolbar button and “Add Comment Block” menu item are disabled. - PC Access refuses to save any block with 3+ AND-IFs, an OR clause, or a comment.
- The panel’s program table can only contain ProgType values 0-4.
The user-facing panel from this repository runs firmware 2.16A, so
all programs in its .pca are compact-form by necessity. Our test
captures of the multi-record form were produced by using PC Access’s
Account → Version Override → “Controller Firmware >= 3.0”, which
spoofs the capability for editing only; the resulting .pca won’t
load on a real 2.16A panel.
Per-record byte layouts (firmware ≥3.0 multi-record form)
Section titled “Per-record byte layouts (firmware ≥3.0 multi-record form)”WHEN (ProgType=5) — event trigger
Section titled “WHEN (ProgType=5) — event trigger”| Byte(s) | Field | Notes |
|---|---|---|
| 0 | prog_type | = 5 |
| 9-10 (BE u16) | event-id | (family \<\< 8) | instance — same encoding as compact-form EVENT’s bytes 9-10 in wire form. No Mon/Day file-form swap. |
| 1-8, 11-13 | zeros | (action lives in a separate THEN record) |
AT (ProgType=6) — single-occurrence time trigger
Section titled “AT (ProgType=6) — single-occurrence time trigger”| Byte(s) | Field | Notes |
|---|---|---|
| 0 | prog_type | = 6 |
| 9 | month | calendar month |
| 10 | day | calendar day |
| 11 | days | Days bitmask (bit 1=Mon … bit 7=Sun) |
| 12 | hour | 0-23, or 25/26 for sunrise/sunset (see TIMED section) |
| 13 | minute | 0-59, or signed for sunrise/sunset offset |
| 1-8 | zeros | (action lives in a separate THEN record) |
Same time-field layout as compact-form TIMED, just with cmd/par/pr2 zero.
EVERY (ProgType=7) — recurring trigger
Section titled “EVERY (ProgType=7) — recurring trigger”| Byte(s) | Field | Notes |
|---|---|---|
| 0 | prog_type | = 7 |
| 3-4 (BE u16) | interval | recurrence value (e.g. 5 for “5 SECONDS” UI default) |
| 1-2, 5-13 | zeros | (unit of interval not yet RE’d — seconds/minutes/hours likely fixed by value range) |
AND (ProgType=8) — one AND-IF condition
Section titled “AND (ProgType=8) — one AND-IF condition”For the traditional case (no structured comparison operator):
| Byte(s) | Field | Notes |
|---|---|---|
| 0 | prog_type | = 8 |
| 1 | family + operand | High byte of the compact-form cond u16 (see cond bit split). e.g. 0x04 = ZONE+SECURE, 0x0A = CTRL+ON, 0x00 = OTHER family. |
| 3-4 (BE u16) | instance | Object number: zone#, unit#, MiscConditional value, etc. |
| 2, 5-13 | zeros | (Bytes 5-13 are populated for structured comparison operators like “TEMP > 70” — not yet fully RE’d.) |
OR (ProgType=9) — separator between AND-groups
Section titled “OR (ProgType=9) — separator between AND-groups”| Byte | Value |
|---|---|
| 0 | 0x09 (OR ProgType) |
| 1-13 | all zero |
Pure discriminator — carries no payload. Marks the boundary between alternative AND-IF groups within a block.
THEN (ProgType=10) — action
Section titled “THEN (ProgType=10) — action”Same cmd / par / pr2 layout as compact-form records (cmd at byte 5, par at byte 6, pr2 at bytes 7-8 LE).
Structured-OP AND records (OP > 0)
Section titled “Structured-OP AND records (OP > 0)”The AND record has two modes distinguished by the OP byte:
- Traditional (
OP == 0): the simple-AND-IF case covered in the table above. Byte 2 holds aProgramCondfamily code (ZONE=4, CTRL=8, …) and bytes 3-4 holdinstance << 8. - Structured (
OP > 0): comparison-operator conditions likeDATE IS EQUAL TO 12/31,TEMPERATURE > 70,ZONE IS BYPASSED.
Per the clsProgram.cs:326-436 accessors, a structured AND record’s
14 bytes decompose as:
Byte(s) (in-memory Data[]) | Field | Type |
|---|---|---|
| 1 | OP | byte (enuCondOP) |
| 2 | Arg1_ArgType | byte (enuCondArgType) |
| 3-4 | Arg1_IX | u16 |
| 5 | Arg1_Field | byte |
| 6 | Arg2_ArgType | byte |
| 7-8 | Arg2_IX | u16 |
| 9 | Arg2_Field | byte |
| 10-11 | CompConst | u16 |
enuCondOP (byte 1):
| Value | Name | UI label |
|---|---|---|
| 0 | Arg1_Traditional | (simple condition, see table above) |
| 1 | Arg1_EQ_Arg2 | IS EQUAL TO |
| 2 | Arg1_NE_Arg2 | IS NOT EQUAL TO |
| 3 | Arg1_LT_Arg2 | IS LESS THAN |
| 4 | Arg1_GT_Arg2 | IS GREATER THAN |
| 5 | Arg1_Odd | IS ODD |
| 6 | Arg1_Even | IS EVEN |
| 7 | Arg1_Multiple_Arg2 | IS A MULTIPLE OF |
| 8 | Arg1_IN_Arg2 | IS IN |
| 9 | Arg1_NOT_IN_Arg2 | IS NOT IN |
enuCondArgType (bytes 2, 6):
| Value | Name |
|---|---|
| 0 | Constant |
| 1 | UserSetting |
| 2 | Zone |
| 3 | Unit |
| 4 | Thermostat |
| 5 | Auxillary |
| 6 | Area |
| 7 | TimeDate |
| 8 | Audio |
| 9 | AccessControl |
| 10 | Message |
| 11 | System |
Worked example: AND IF DATE IS EQUAL TO 12/31
Section titled “Worked example: AND IF DATE IS EQUAL TO 12/31”Authored as a 5-line block, the AND record landed at:
08 07 01 00 00 01 00 1f 0c 00 00 00 00 00Tracing the byte flow through Read (LE u16) → setter (swap to BE in Data[]) → accessor (BE read):
| Byte position | Disk | Read u16 LE | After setter (Data[]) | Accessor reads |
|---|---|---|---|---|
| 1-2 | 07 01 | Cond=0x0107 | Data[1]=01, Data[2]=07 | OP=1 (Arg1_EQ_Arg2), Arg1_ArgType=7 (TimeDate) |
| 3-4 | 00 00 | Cond2=0x0000 | Data[3]=00, Data[4]=00 | Arg1_IX=0 (CURRENT_DATE) |
| 5 | 01 | — | Data[5]=01 | Arg1_Field=1 |
| 6 | 00 | — | Data[6]=00 | Arg2_ArgType=0 (Constant) |
| 7-8 | 1f 0c | Pr2=0x0c1f | Data[7]=0c, Data[8]=1f | Arg2_IX=0x0c1f = (month=12, day=31) |
| 9 | 00 | — | Data[9]=00 | Arg2_Field=0 |
| 10-11 | 00 00 | — | Data[10]=00, Data[11]=00 | CompConst=0 |
How Traditional and Structured share the same byte slots
Section titled “How Traditional and Structured share the same byte slots”The C# clsConditionLine.Cond property at clsConditionLine.cs:17-33
is the bridge between the two modes:
public ushort Cond { get { if (OP == enuCondOP.Arg1_Traditional) return ((Arg1.ArgType << 8) | (Arg1.IX >> 8)); return 0; } set { OP = enuCondOP.Arg1_Traditional; Arg1.ArgType = (enuCondArgType)(value >> 8); Arg1.IX = (ushort)((value & 0xFF) << 8); }}When you set Cond = 0x0405 for “Zone 5 Secure”:
Arg1.ArgTypereceives0x04(the family code value, stored as a raw byte — semantically aProgramCond.ZONEeven though the field type isenuCondArgType)Arg1.IXreceives(0x05 << 8) = 0x0500(the instance number shifted up by 8 bits)
These get serialised through the same clsProgram.Cond /
Cond2 setters as compact-form records, ending up on disk as
04 00 00 05 at bytes 1-4. The same disk slots therefore double
as:
OP(byte 2 LE →Data[1]) andArg1_ArgType(byte 1 LE →Data[2]) for structured records, ORfamily << 8 | instance >> 8packed into theCondu16 for Traditional records (using the same byte storage but interpreted via theclsConditionLine.Condsynthesis)
The omni_pca.Program class exposes and_op, and_arg1_argtype,
and_arg1_ix, and_arg2_ix etc. as direct accessors for the
structured case, plus and_family / and_instance as
Traditional-friendly aliases (with and_instance doing the right
shift for the Traditional case automatically).
Other open items
Section titled “Other open items”DoubleProgramConditionalcapability flag. We hardcode 14-byte records for the Omni Pro II. Other models clear the flag and use 12-byte records with onlycond(nocond2). Adding support is its own follow-up.
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).
Decoding multi-record programs
Section titled “Decoding multi-record programs”For records with prog_type in the firmware-≥3.0 range (5-10), the
Program class exposes type-specific accessor properties:
from omni_pca.programs import Program, ProgramType, ProgramCond
p = Program.from_file_record(record_bytes)
if p.is_multi_record(): # WHEN (5) — event trigger if p.prog_type == ProgramType.WHEN: event_id = p.event_id # 0x0405 for Zone 5 Secure family = (event_id >> 8) & 0xFC # ProgramCond.ZONE = 0x04 instance = event_id & 0xFF # zone # 5
# AT (6) — single-occurrence time trigger elif p.prog_type == ProgramType.AT: # Same time fields as compact TIMED when = (p.hour, p.minute) days = p.days # Days bitmask
# EVERY (7) — recurring trigger elif p.prog_type == ProgramType.EVERY: interval = p.every_interval # e.g. 5 for "5 SECONDS"
# AND (8) — one AND-IF condition (either Traditional or Structured) elif p.prog_type == ProgramType.AND: if p.and_op == 0: # Traditional: family code at byte 1, instance at byte 4 family = p.and_family # 0x04=ZONE, 0x08=CTRL, 0x0A=CTRL+ON, ... operand = (family & 0x02) >> 1 # ON / NOT_READY / ENABLED instance = p.and_instance # zone#, unit#, MiscConditional value else: # Structured: e.g. "AND IF DATE IS EQUAL TO 12/31" op_ = p.and_op # enuCondOP (1=EQ, 2=NE, 3=LT, 4=GT, ...) arg1_type = p.and_arg1_argtype # enuCondArgType (TimeDate=7, ...) arg1_ix = p.and_arg1_ix # object index or sub-field code arg1_fld = p.and_arg1_field arg2_type = p.and_arg2_argtype # often Constant (=0) arg2_ix = p.and_arg2_ix # constant value or 2nd object arg2_fld = p.and_arg2_field compconst = p.and_compconst
# OR (9) — pure separator, no fields elif p.prog_type == ProgramType.OR: pass # only the prog_type byte matters
# THEN (10) — action elif p.prog_type == ProgramType.THEN: cmd = p.cmd # enuUnitCommand par = p.par pr2 = p.pr2 # object number (LE u16, same as compact action)A complete multi-record block is a contiguous run of these records
in the program table — one record per visual line of the PC Access
program-block editor. Use is_multi_record() to detect the range
boundaries when iterating.
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.